From cbf1bcf8e0669dc17415b5e230570bb183b0f7b0 Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 8 May 2026 10:58:28 +0100 Subject: [PATCH 1/6] fix(transaction-pay-controller): account override fixes (#8724) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes in `transaction-pay-controller`'s Relay strategy that together unblock Money Account deposits via MetaMask Pay (and any other delegation-based deposit flow targeting Arbitrum USDC). 1. Funding-leg recipient should be `transaction.txParams.from`, not `request.from` 2. Arbitrum-USDC → Hypercore rewrite must be gated on `perpsDeposit` Ref: https://consensyssoftware.atlassian.net/browse/CONF-1324 --- > [!NOTE] > **Medium Risk** > Adjusts Relay quote transaction construction and chain/token rewrite behavior for Arbitrum USDC deposits; mistakes here could misroute funds or produce incorrect quotes for delegation-based flows. > > **Overview** > Fixes Relay quoting for delegation-based flows when `accountOverride` causes `request.from` to differ from the executing delegator. > > Relay now funds the **delegator address** (`transaction.txParams.from`) when building the initial token-transfer leg (falling back to `request.from` if unset), and the Arbitrum-USDC → Hypercore (Hyperliquid) quote rewrite is now **only applied for `TransactionType.perpsDeposit`** (not for other transaction types or post-quote flows). Tests and the changelog are updated to cover these cases. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 882ea79b1d9a512f9c4d16a5a31b88376fcd2baf. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Jyoti Puri --- .../transaction-pay-controller/CHANGELOG.md | 6 ++ .../src/strategy/relay/relay-quotes.test.ts | 98 ++++++++++++++++++- .../src/strategy/relay/relay-quotes.ts | 22 ++++- 3 files changed, 122 insertions(+), 4 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 8602d1ac9f..8d4fd456d3 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fix transaction with accountOverride and targeting Arbitrum USDC ([#8724](https://github.com/MetaMask/core/pull/8724)) + - Deliver the Relay-acquired token to `transaction.txParams.from` (the delegator that executes the inner delegation), not `request.from`. + - Gate the Arbitrum-USDC → Hypercore quote rewrite on `transaction.type === TransactionType.perpsDeposit`. + ## [22.0.2] ### Changed diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts index 54b46d0732..f5d7c72a2f 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.test.ts @@ -454,6 +454,68 @@ describe('Relay Quotes Utils', () => { ); }); + it('funds the delegator (transaction.txParams.from) rather than request.from when they differ', async () => { + const delegatorAddress = + '0xabcdef0000000000000000000000000000000001' as Hex; + + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + from: delegatorAddress, + data: '0xabc' as Hex, + }, + } as TransactionMeta, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.txs[0]).toStrictEqual({ + to: QUOTE_REQUEST_MOCK.targetTokenAddress, + data: '0xa9059cbb000000000000000000000000abcdef0000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000007b', + value: '0x0', + }); + }); + + it('falls back to request.from for the funding recipient when transaction.txParams.from is unset', async () => { + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [QUOTE_REQUEST_MOCK], + transaction: { + ...TRANSACTION_META_MOCK, + txParams: { + data: '0xabc' as Hex, + }, + } as TransactionMeta, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body.txs[0]).toStrictEqual({ + to: QUOTE_REQUEST_MOCK.targetTokenAddress, + data: '0xa9059cbb0000000000000000000000001234567890123456789012345678901234567891000000000000000000000000000000000000000000000000000000000000007b', + value: '0x0', + }); + }); + it('includes request in quote', async () => { successfulFetchMock.mockResolvedValue({ ok: true, @@ -2751,7 +2813,10 @@ describe('Relay Quotes Utils', () => { accountSupports7702: true, messenger, requests: [arbitrumToHyperliquidRequest], - transaction: TRANSACTION_META_MOCK, + transaction: { + ...TRANSACTION_META_MOCK, + type: TransactionType.perpsDeposit, + }, }); const body = JSON.parse( @@ -2767,6 +2832,37 @@ describe('Relay Quotes Utils', () => { ); }); + it('does not convert to Hyperliquid deposit when parent transaction is not a Perps deposit', async () => { + const arbitrumUsdcRequest: QuoteRequest = { + ...QUOTE_REQUEST_MOCK, + targetChainId: CHAIN_ID_ARBITRUM, + targetTokenAddress: ARBITRUM_USDC_ADDRESS, + }; + + successfulFetchMock.mockResolvedValue({ + ok: true, + json: async () => QUOTE_MOCK, + } as never); + + await getRelayQuotes({ + accountSupports7702: true, + messenger, + requests: [arbitrumUsdcRequest], + transaction: TRANSACTION_META_MOCK, + }); + + const body = JSON.parse( + successfulFetchMock.mock.calls[0][1]?.body as string, + ); + + expect(body).toStrictEqual( + expect.objectContaining({ + destinationChainId: Number(CHAIN_ID_ARBITRUM), + destinationCurrency: ARBITRUM_USDC_ADDRESS, + }), + ); + }); + it('does not convert to Hyperliquid deposit for post-quote requests targeting Arbitrum USDC', async () => { const postQuoteRequest: QuoteRequest = { ...QUOTE_REQUEST_MOCK, diff --git a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts index 3ca5e23e96..14b95c089d 100644 --- a/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/relay/relay-quotes.ts @@ -2,6 +2,7 @@ import { Interface } from '@ethersproject/abi'; import { toHex } from '@metamask/controller-utils'; +import { TransactionType } from '@metamask/transaction-controller'; import type { TransactionMeta } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; @@ -87,7 +88,9 @@ export async function getRelayQuotes( return hasTargetMinimum || isPostQuote || isExactInputRequest; }) - .map((singleRequest) => normalizeRequest(singleRequest)); + .map((singleRequest) => + normalizeRequest(singleRequest, request.transaction), + ); log('Normalized requests', normalizedRequests); @@ -346,10 +349,15 @@ async function processTransactions( requestBody.refundTo = request.from; } + const fundingRecipient = (transaction.txParams?.from as Hex) ?? request.from; + requestBody.txs = [ { to: request.targetTokenAddress, - data: buildTokenTransferData(request.from, request.targetAmountMinimum), + data: buildTokenTransferData( + fundingRecipient, + request.targetAmountMinimum, + ), value: '0x0', }, { @@ -364,14 +372,22 @@ async function processTransactions( * Normalizes requests for Relay. * * @param request - Quote request to normalize. + * @param transaction - Parent transaction metadata, used to gate + * Hyperliquid-specific rewrites on transaction type. * @returns Normalized request. */ -function normalizeRequest(request: QuoteRequest): QuoteRequest { +function normalizeRequest( + request: QuoteRequest, + transaction: TransactionMeta, +): QuoteRequest { const newRequest = { ...request, }; + const isPerpsDeposit = transaction.type === TransactionType.perpsDeposit; + const isHyperliquidDeposit = + isPerpsDeposit && !request.isPostQuote && request.targetChainId === CHAIN_ID_ARBITRUM && request.targetTokenAddress.toLowerCase() === From 687f9ea3e61426d0d0793a2ed9dfefa5e46336a7 Mon Sep 17 00:00:00 2001 From: Jyoti Puri Date: Fri, 8 May 2026 15:42:31 +0530 Subject: [PATCH 2/6] Release/969.0.0 (#8745) ## Explanation Release PR for transaction-pay-controller. ## References Related to: https://consensyssoftware.atlassian.net/browse/CONF-1324 ## Checklist - [X] I've updated the test suite for new or updated code as appropriate - [X] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [X] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [X] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Low Risk** > Release metadata-only changes (version bumps and changelog/link updates) with no runtime logic modifications. > > **Overview** > Bumps the monorepo version to `969.0.0` and releases `@metamask/transaction-pay-controller` `22.1.0`. > > Updates the `transaction-pay-controller` changelog for `22.1.0` (documenting the Arbitrum USDC/accountOverride fix) and adjusts the compare links to point `Unreleased` at the new tag. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit f1e23fe0fbb64cb4200cf50ee6e3a1d7647044dd. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- package.json | 2 +- packages/transaction-pay-controller/CHANGELOG.md | 5 ++++- packages/transaction-pay-controller/package.json | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 35a6c177c0..a538b91dda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "968.0.0", + "version": "969.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 8d4fd456d3..50eb467062 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [22.1.0] + ### Fixed - Fix transaction with accountOverride and targeting Arbitrum USDC ([#8724](https://github.com/MetaMask/core/pull/8724)) @@ -812,7 +814,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#6820](https://github.com/MetaMask/core/pull/6820)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.0.2...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.1.0...HEAD +[22.1.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.0.2...@metamask/transaction-pay-controller@22.1.0 [22.0.2]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.0.1...@metamask/transaction-pay-controller@22.0.2 [22.0.1]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@22.0.0...@metamask/transaction-pay-controller@22.0.1 [22.0.0]: https://github.com/MetaMask/core/compare/@metamask/transaction-pay-controller@21.1.0...@metamask/transaction-pay-controller@22.0.0 diff --git a/packages/transaction-pay-controller/package.json b/packages/transaction-pay-controller/package.json index a6d76b80f4..2995ce0fd6 100644 --- a/packages/transaction-pay-controller/package.json +++ b/packages/transaction-pay-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/transaction-pay-controller", - "version": "22.0.2", + "version": "22.1.0", "description": "Manages alternate payment strategies to provide required funds for transactions in MetaMask", "keywords": [ "Ethereum", From 374c0ededae5ba21f1684de50a0f89397197649b Mon Sep 17 00:00:00 2001 From: Matthew Walsh Date: Fri, 8 May 2026 11:30:48 +0100 Subject: [PATCH 3/6] fix(transaction-controller): reject transactions that would deploy an empty contract (#8744) ## Explanation `TransactionController` would silently classify a transaction with no recipient and no real bytecode as a contract deployment and broadcast it, creating an empty contract on-chain with any `value` permanently locked inside. The root cause is that `'0x'` is a non-empty string, so `if (data)` truthiness checks in `validateParamRecipient` and `determineTransactionType` treated it as real deployment bytecode. `validateParamRecipient` also didn't recognize `to === ''` as missing. Validation now treats `to === ''` as missing alongside `'0x'` / `undefined`, and both the validator and the type classifier require `data.length > 2` before accepting a missing recipient as a legitimate deployment. ## References ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Medium Risk** > Tightens core transaction validation/type inference around contract deployments; could cause some previously-accepted malformed transactions to be rejected or reclassified, but the change is narrowly scoped and well-covered by tests. > > **Overview** > Prevents accidental empty contract deployments by treating missing recipients (`to` of `''`, `'0x'`, or `undefined`) as **invalid unless** `data` contains *real* deployment bytecode (more than just `'0x'`). > > Updates both `validateTxParams`/`validateParamRecipient` and `determineTransactionType` to require non-empty bytecode before allowing a missing `to`, adds targeted unit tests for the new edge cases, and records the fix in the `transaction-controller` changelog. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7154b2fa05816eaa3197f95221bdcd04634c9a68. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/transaction-controller/CHANGELOG.md | 4 ++ .../src/utils/transaction-type.test.ts | 30 +++++++- .../src/utils/transaction-type.ts | 4 +- .../src/utils/validation.test.ts | 72 +++++++++++++++++-- .../src/utils/validation.ts | 27 +++++-- 5 files changed, 124 insertions(+), 13 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 2d2da9c929..e5dbfcb379 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Trigger the first-time-interaction warning correctly for `safeTransferFrom` token transfers by including `TransactionType.tokenMethodSafeTransferFrom` in the effective-recipient decoding logic ([#8723](https://github.com/MetaMask/core/pull/8723)) +### Fixed + +- Reject transactions with a missing `to` and empty `data` to prevent accidental empty contract deployments ([#8744](https://github.com/MetaMask/core/pull/8744)) + ## [65.2.0] ### Added diff --git a/packages/transaction-controller/src/utils/transaction-type.test.ts b/packages/transaction-controller/src/utils/transaction-type.test.ts index a5e33682cd..2b6b57bee3 100644 --- a/packages/transaction-controller/src/utils/transaction-type.test.ts +++ b/packages/transaction-controller/src/utils/transaction-type.test.ts @@ -165,7 +165,7 @@ describe('determineTransactionType', () => { }); }); - it('returns a contract deployment type when "to" is falsy and there is data', async () => { + it('returns a contract deployment type when "to" is falsy and there is real bytecode', async () => { const result = await determineTransactionType( { ...txParams, @@ -180,6 +180,34 @@ describe('determineTransactionType', () => { }); }); + it('does NOT classify as deployContract when "to" is falsy but data is "0x" (empty bytecode)', async () => { + jest.mocked(rpcRequest).mockResolvedValue('0x'); + + const result = await determineTransactionType( + { + ...txParams, + to: '', + data: '0x', + }, + { messenger: messengerMock, networkClientId: networkClientIdMock }, + ); + expect(result.type).not.toBe(TransactionType.deployContract); + }); + + it('does NOT classify as deployContract when "to" is undefined but data is "0x"', async () => { + jest.mocked(rpcRequest).mockResolvedValue('0x'); + + const result = await determineTransactionType( + { + ...txParams, + to: undefined, + data: '0x', + }, + { messenger: messengerMock, networkClientId: networkClientIdMock }, + ); + expect(result.type).not.toBe(TransactionType.deployContract); + }); + it('returns a simple send type with a 0x getCodeResponse when there is data, but the "to" address is not a contract address', async () => { jest.mocked(rpcRequest).mockResolvedValue('0x'); diff --git a/packages/transaction-controller/src/utils/transaction-type.ts b/packages/transaction-controller/src/utils/transaction-type.ts index 1832354a1d..85ccad46cc 100644 --- a/packages/transaction-controller/src/utils/transaction-type.ts +++ b/packages/transaction-controller/src/utils/transaction-type.ts @@ -41,7 +41,9 @@ export async function determineTransactionType( ): Promise { const { data, to } = txParams; - if (data && !to) { + const hasRealBytecode = Boolean(data && data !== '0x' && data.length > 2); + + if (hasRealBytecode && !to) { return { type: TransactionType.deployContract, getCodeResponse: undefined }; } diff --git a/packages/transaction-controller/src/utils/validation.test.ts b/packages/transaction-controller/src/utils/validation.test.ts index 006f25998d..c9a2f53d64 100644 --- a/packages/transaction-controller/src/utils/validation.test.ts +++ b/packages/transaction-controller/src/utils/validation.test.ts @@ -77,7 +77,11 @@ describe('validation', () => { ); }); - it('should throw if no data', () => { + it('should throw if to is missing and no real bytecode', () => { + const expectedError = rpcErrors.invalidParams( + 'Invalid "to" address: must be specified for transactions without contract deployment bytecode.', + ); + expect(() => validateTxParams({ from: FROM_MOCK, @@ -85,7 +89,7 @@ describe('validation', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any), - ).toThrow(rpcErrors.invalidParams('Invalid "to" address.')); + ).toThrow(expectedError); expect(() => validateTxParams({ @@ -93,12 +97,60 @@ describe('validation', () => { // TODO: Replace `any` with type // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any), - ).toThrow(rpcErrors.invalidParams('Invalid "to" address.')); + ).toThrow(expectedError); + }); + + it('should throw if to is empty string and no real bytecode', () => { + expect(() => + validateTxParams({ + from: FROM_MOCK, + to: '' as Hex, + data: '0x', + value: '0x1', + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid "to" address: must be specified for transactions without contract deployment bytecode.', + ), + ); }); - it('should delete data', () => { + it('should throw if to is empty string and data is "0x" (would deploy empty contract)', () => { + expect(() => + validateTxParams({ + from: FROM_MOCK, + to: '' as Hex, + data: '0x', + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid "to" address: must be specified for transactions without contract deployment bytecode.', + ), + ); + }); + + it('should throw if to is undefined and data is "0x" (would deploy empty contract)', () => { + expect(() => + validateTxParams({ + from: FROM_MOCK, + data: '0x', + // TODO: Replace `any` with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any), + ).toThrow( + rpcErrors.invalidParams( + 'Invalid "to" address: must be specified for transactions without contract deployment bytecode.', + ), + ); + }); + + it('should remove "to" when missing and data has real bytecode (legitimate deployment)', () => { const transaction = { - data: 'foo', + data: '0x608060405234', from: TO_MOCK, to: '0x', }; @@ -106,6 +158,16 @@ describe('validation', () => { expect(transaction.to).toBeUndefined(); }); + it('should remove "to" when empty string and data has real bytecode', () => { + const transaction = { + data: '0x608060405234', + from: TO_MOCK, + to: '' as Hex, + }; + validateTxParams(transaction); + expect(transaction.to).toBeUndefined(); + }); + it('should throw if invalid to address', () => { expect(() => validateTxParams({ diff --git a/packages/transaction-controller/src/utils/validation.ts b/packages/transaction-controller/src/utils/validation.ts index 68dea00a73..a523fb2e82 100644 --- a/packages/transaction-controller/src/utils/validation.ts +++ b/packages/transaction-controller/src/utils/validation.ts @@ -205,16 +205,31 @@ function validateParamValue(value?: string): void { * * @param txParams - The transaction parameters object to validate. * @throws Throws an error if the recipient address is invalid: - * - If the recipient address is an empty string ('0x') or undefined and the transaction contains data, - * the "to" field is removed from the transaction parameters. - * - If the recipient address is not a valid hexadecimal Ethereum address, an error is thrown. + * - If the recipient address is missing (empty string, '0x', or undefined) and the + * transaction does not contain real bytecode (data must be longer than `0x`), + * an error is thrown. This prevents accidental contract deployments with empty + * `to` and empty `data` from locking funds. + * - If the recipient address is missing and the transaction contains real + * bytecode (data longer than `0x`), the "to" field is removed from the + * transaction parameters (legitimate contract deployment). + * - If the recipient address is not a valid hexadecimal Ethereum address, an + * error is thrown. */ function validateParamRecipient(txParams: TransactionParams): void { - if (txParams.to === '0x' || txParams.to === undefined) { - if (txParams.data) { + const isMissingRecipient = + txParams.to === '0x' || txParams.to === '' || txParams.to === undefined; + + if (isMissingRecipient) { + const hasRealBytecode = Boolean( + txParams.data && txParams.data !== '0x' && txParams.data.length > 2, + ); + + if (hasRealBytecode) { delete txParams.to; } else { - throw rpcErrors.invalidParams(`Invalid "to" address.`); + throw rpcErrors.invalidParams( + `Invalid "to" address: must be specified for transactions without contract deployment bytecode.`, + ); } } else if (txParams.to !== undefined && !isValidHexAddress(txParams.to)) { throw rpcErrors.invalidParams(`Invalid "to" address.`); From c7dd040c6090fc00eec373703493dbb2619f3df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20G=C3=B6ktu=C4=9F=20Poyraz?= Date: Fri, 8 May 2026 14:24:01 +0200 Subject: [PATCH 4/6] feat: Derive fiat asset from feature flags before hardcoded fallback (#8631) ## Explanation Currently `deriveFiatAssetForFiatPayment` resolves the fiat asset for a payment entirely from a hardcoded map (`FIAT_ASSET_ID_BY_TX_TYPE`). This makes it impossible to adjust asset mappings without a code release. This PR introduces a 3-tier resolution for fiat assets: 1. **Feature flag** - reads from `confirmations_pay_fiat.assetPerTransactionType[txType]` via `RemoteFeatureFlagController` 2. **Hardcoded map** - falls back to the existing `FIAT_ASSET_ID_BY_TX_TYPE` constant 3. **ETH on mainnet** - terminal fallback when neither source has an entry The function signature gains a `messenger` parameter (already available at the call site in `fiat-quotes.ts`), and the return type tightens from `TransactionPayFiatAsset | undefined` to `TransactionPayFiatAsset` since the ETH mainnet fallback guarantees a value. ## References - Feature flag key: `confirmations_pay_fiat` ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Medium Risk** > Medium risk because it changes fiat on-ramp asset resolution and CAIP asset identifiers used for quote fetching and order validation, which can affect MM Pay fiat quote/submit flows across transaction types. > > **Overview** > **Fiat-asset selection is now remotely configurable.** Fiat strategy resolves the source fiat asset per transaction type from the `confirmations_pay_fiat.assetPerTransactionType` remote feature flag, falls back to the existing hardcoded map, and finally defaults to *mainnet ETH*. > > **CAIP asset identifiers are now derived, not stored.** `TransactionPayFiatAsset` no longer carries `caipAssetId`/`decimals`; call sites now build CAIP-19 via new `buildCaipAssetType(chainId, address)` and fetch decimals via `getTokenInfo`, updating ramps token selection, quote requests, and fiat order validation accordingly, with tests updated to cover the new resolution and fallbacks. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 15f1c3d9ace41f7f23e46b91238496ef6dec9409. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .../transaction-pay-controller/CHANGELOG.md | 4 + .../src/TransactionPayController.test.ts | 4 +- .../src/TransactionPayController.ts | 8 +- .../src/constants.ts | 5 + .../src/strategy/fiat/constants.ts | 13 +- .../src/strategy/fiat/fiat-quotes.test.ts | 25 +++- .../src/strategy/fiat/fiat-quotes.ts | 33 ++-- .../src/strategy/fiat/fiat-submit.test.ts | 20 ++- .../src/strategy/fiat/fiat-submit.ts | 27 +++- .../src/strategy/fiat/utils.test.ts | 141 +++++++++++++++++- .../src/strategy/fiat/utils.ts | 33 ++-- .../src/utils/feature-flags.test.ts | 104 +++++++++++++ .../src/utils/feature-flags.ts | 44 ++++++ .../src/utils/token.test.ts | 39 +++++ .../src/utils/token.ts | 39 ++++- 15 files changed, 470 insertions(+), 69 deletions(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 50eb467062..79b23ddb2c 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Resolve fiat asset per transaction type from `confirmations_pay_fiat` remote feature flag, falling back to hardcoded map then ETH on mainnet ([#8631](https://github.com/MetaMask/core/pull/8631)) + ## [22.1.0] ### Fixed diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index e857a98390..03ac1f83c9 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -710,9 +710,7 @@ describe('TransactionPayController', () => { const CAIP_ASSET_ID_MOCK = 'eip155:137/slip44:966'; const FIAT_ASSET_MOCK = { address: '0x0000000000000000000000000000000000001010' as Hex, - caipAssetId: CAIP_ASSET_ID_MOCK, chainId: '0x89' as Hex, - decimals: 18, }; let setSelectedTokenMock: jest.Mock; @@ -788,7 +786,7 @@ describe('TransactionPayController', () => { it('does not call setSelectedToken when fiat asset cannot be derived', () => { getTransactionMock.mockReturnValue(TRANSACTION_META_MOCK); - deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined); + deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined as never); const updateTransactionData = getUpdateTransactionData(); diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 19d4fdfb6f..8da403c6f1 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -26,6 +26,7 @@ import type { import { getStrategyOrder } from './utils/feature-flags'; import { updateQuotes } from './utils/quotes'; import { updateSourceAmounts } from './utils/source-amounts'; +import { buildCaipAssetType } from './utils/token'; import { getTransaction, subscribeAssetChanges, @@ -294,12 +295,15 @@ export class TransactionPayController extends BaseController< transactionId, this.messenger, ) as TransactionMeta; - const fiatAsset = deriveFiatAssetForFiatPayment(transaction); + const fiatAsset = deriveFiatAssetForFiatPayment( + transaction, + this.messenger, + ); if (fiatAsset) { try { this.messenger.call( 'RampsController:setSelectedToken', - fiatAsset.caipAssetId, + buildCaipAssetType(fiatAsset.chainId, fiatAsset.address), ); } catch { // Intentionally no-op — tokens may not be loaded in RampsController yet. diff --git a/packages/transaction-pay-controller/src/constants.ts b/packages/transaction-pay-controller/src/constants.ts index 5a097ee85d..c740bdbd6d 100644 --- a/packages/transaction-pay-controller/src/constants.ts +++ b/packages/transaction-pay-controller/src/constants.ts @@ -2,6 +2,7 @@ import type { Hex } from '@metamask/utils'; export const CONTROLLER_NAME = 'TransactionPayController'; export const CHAIN_ID_ARBITRUM = '0xa4b1' as Hex; +export const CHAIN_ID_MAINNET = '0x1' as Hex; export const CHAIN_ID_POLYGON = '0x89' as Hex; export const CHAIN_ID_HYPERCORE = '0x539' as Hex; @@ -19,6 +20,10 @@ export const HYPERCORE_USDC_ADDRESS = '0x00000000000000000000000000000000'; export const HYPERCORE_USDC_DECIMALS = 8; export const USDC_DECIMALS = 6; +export const SLIP44_COIN_TYPE_BY_CHAIN: Record = { + [CHAIN_ID_POLYGON]: 966, // POL +}; + export const STABLECOINS: Record = { // Mainnet '0x1': [ diff --git a/packages/transaction-pay-controller/src/strategy/fiat/constants.ts b/packages/transaction-pay-controller/src/strategy/fiat/constants.ts index 5289ebcb82..83d85afa3d 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/constants.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/constants.ts @@ -3,6 +3,7 @@ import type { Hex } from '@metamask/utils'; import { CHAIN_ID_ARBITRUM, + CHAIN_ID_MAINNET, CHAIN_ID_POLYGON, NATIVE_TOKEN_ADDRESS, } from '../../constants'; @@ -11,26 +12,24 @@ export const DEFAULT_FIAT_CURRENCY = 'USD'; export type TransactionPayFiatAsset = { address: Hex; - caipAssetId: string; chainId: Hex; - decimals: number; }; const POLYGON_POL_FIAT_ASSET: TransactionPayFiatAsset = { address: '0x0000000000000000000000000000000000001010', - caipAssetId: 'eip155:137/slip44:966', chainId: CHAIN_ID_POLYGON, - decimals: 18, }; const ARBITRUM_ETH_FIAT_ASSET: TransactionPayFiatAsset = { address: NATIVE_TOKEN_ADDRESS, - caipAssetId: 'eip155:42161/slip44:60', chainId: CHAIN_ID_ARBITRUM, - decimals: 18, }; -// We might use feature flags to determine these later. +export const ETH_MAINNET_FIAT_ASSET: TransactionPayFiatAsset = { + address: NATIVE_TOKEN_ADDRESS, + chainId: CHAIN_ID_MAINNET, +}; + export const FIAT_ASSET_ID_BY_TX_TYPE: Partial< Record > = { diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts index 0bc9d291fd..5a5766b3af 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.test.ts @@ -13,7 +13,12 @@ import type { TransactionPayQuote, TransactionPayRequiredToken, } from '../../types'; -import { computeRawFromFiatAmount, getTokenFiatRate } from '../../utils/token'; +import { + buildCaipAssetType, + computeRawFromFiatAmount, + getTokenFiatRate, + getTokenInfo, +} from '../../utils/token'; import { getRelayQuotes } from '../relay/relay-quotes'; import type { RelayQuote } from '../relay/types'; import type { TransactionPayFiatAsset } from './constants'; @@ -52,9 +57,7 @@ const REQUIRED_TOKEN_MOCK: TransactionPayRequiredToken = { const FIAT_ASSET_MOCK: TransactionPayFiatAsset = { address: '0x0000000000000000000000000000000000001010', - caipAssetId: 'eip155:137/slip44:966', chainId: '0x89', - decimals: 18, }; const FIAT_QUOTE_MOCK: RampsQuote = { @@ -199,9 +202,13 @@ function getRequest({ }; } +const FIAT_ASSET_CAIP_ID_MOCK = 'eip155:137/slip44:966'; + describe('getFiatQuotes', () => { + const buildCaipAssetTypeMock = jest.mocked(buildCaipAssetType); const getRelayQuotesMock = jest.mocked(getRelayQuotes); const getTokenFiatRateMock = jest.mocked(getTokenFiatRate); + const getTokenInfoMock = jest.mocked(getTokenInfo); const computeRawFromFiatAmountMock = jest.mocked(computeRawFromFiatAmount); const deriveFiatAssetForFiatPaymentMock = jest.mocked( deriveFiatAssetForFiatPayment, @@ -210,11 +217,13 @@ describe('getFiatQuotes', () => { beforeEach(() => { jest.resetAllMocks(); + buildCaipAssetTypeMock.mockReturnValue(FIAT_ASSET_CAIP_ID_MOCK); deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK); getTokenFiatRateMock.mockReturnValue({ fiatRate: '2', usdRate: '2', }); + getTokenInfoMock.mockReturnValue({ decimals: 18, symbol: 'POL' }); computeRawFromFiatAmountMock.mockReturnValue('5000000000000000000'); getRelayQuotesMock.mockResolvedValue([getRelayQuoteMock()]); }); @@ -242,7 +251,7 @@ describe('getFiatQuotes', () => { 'RampsController:getQuotes', expect.objectContaining({ amount: 20, - assetId: FIAT_ASSET_MOCK.caipAssetId, + assetId: FIAT_ASSET_CAIP_ID_MOCK, fiat: 'USD', paymentMethods: ['/payments/debit-credit-card'], providers: [SELECTED_PROVIDER_ID], @@ -349,8 +358,8 @@ describe('getFiatQuotes', () => { expect(getRelayQuotesMock).not.toHaveBeenCalled(); }); - it('returns empty array if fiat asset mapping is missing', async () => { - deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined); + it('returns empty array if source token fiat rate is missing', async () => { + getTokenFiatRateMock.mockReturnValue(undefined); const { request } = getRequest(); const result = await getFiatQuotes(request); @@ -359,8 +368,8 @@ describe('getFiatQuotes', () => { expect(getRelayQuotesMock).not.toHaveBeenCalled(); }); - it('returns empty array if source token fiat rate is missing', async () => { - getTokenFiatRateMock.mockReturnValue(undefined); + it('returns empty array if token info is unavailable', async () => { + getTokenInfoMock.mockReturnValue(undefined); const { request } = getRequest(); const result = await getFiatQuotes(request); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts index a4184af3d8..e401447f53 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-quotes.ts @@ -11,7 +11,12 @@ import type { TransactionPayRequiredToken, TransactionPayQuote, } from '../../types'; -import { computeRawFromFiatAmount, getTokenFiatRate } from '../../utils/token'; +import { + buildCaipAssetType, + computeRawFromFiatAmount, + getTokenFiatRate, + getTokenInfo, +} from '../../utils/token'; import { getRelayQuotes } from '../relay/relay-quotes'; import type { RelayQuote } from '../relay/types'; import { DEFAULT_FIAT_CURRENCY } from './constants'; @@ -46,14 +51,9 @@ export async function getFiatQuotes( const amountFiat = transactionData?.fiatPayment?.amountFiat; const walletAddress = transaction.txParams.from as Hex; const requiredTokens = getRequiredTokens(transactionData?.tokens); - const fiatAsset = deriveFiatAssetForFiatPayment(transaction); - - if ( - !amountFiat || - !fiatPaymentMethod || - !requiredTokens.length || - !fiatAsset - ) { + const fiatAsset = deriveFiatAssetForFiatPayment(transaction, messenger); + + if (!amountFiat || !fiatPaymentMethod || !requiredTokens.length) { return []; } @@ -171,7 +171,7 @@ async function getRampsQuote({ const quotes = await messenger.call('RampsController:getQuotes', { amount: adjustedAmount, - assetId: fiatAsset.caipAssetId, + assetId: buildCaipAssetType(fiatAsset.chainId, fiatAsset.address), fiat: DEFAULT_FIAT_CURRENCY, paymentMethods: [fiatPaymentMethod], providers: selectedProviderId ? [selectedProviderId] : undefined, @@ -203,7 +203,6 @@ function buildRelayRequestFromAmountFiat({ fiatAsset: { address: Hex; chainId: Hex; - decimals: number; }; messenger: PayStrategyGetQuotesRequest['messenger']; requiredToken: TransactionPayRequiredToken; @@ -219,9 +218,19 @@ function buildRelayRequestFromAmountFiat({ return undefined; } + const tokenInfo = getTokenInfo( + messenger, + fiatAsset.address, + fiatAsset.chainId, + ); + + if (!tokenInfo) { + return undefined; + } + const sourceAmountRaw = computeRawFromFiatAmount( amountFiat, - fiatAsset.decimals, + tokenInfo.decimals, sourceFiatRate.usdRate, ); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts index 8b94b88f2f..3316f9c537 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.test.ts @@ -14,6 +14,7 @@ import type { QuoteRequest, TransactionPayQuote, } from '../../types'; +import { buildCaipAssetType, getTokenInfo } from '../../utils/token'; import { getRelayQuotes } from '../relay/relay-quotes'; import { submitRelayQuotes } from '../relay/relay-submit'; import type { RelayQuote } from '../relay/types'; @@ -23,6 +24,7 @@ import type { FiatQuote } from './types'; import { deriveFiatAssetForFiatPayment } from './utils'; jest.mock('./utils'); +jest.mock('../../utils/token'); jest.mock('../relay/relay-quotes'); jest.mock('../relay/relay-submit'); @@ -40,9 +42,7 @@ const TRANSACTION_MOCK = { const FIAT_ASSET_MOCK: TransactionPayFiatAsset = { address: '0x0000000000000000000000000000000000001010', - caipAssetId: 'eip155:137/slip44:966', chainId: '0x89', - decimals: 18, }; const RAMPS_QUOTE_MOCK: RampsQuote = { @@ -227,7 +227,11 @@ function getRequest({ }; } +const FIAT_ASSET_CAIP_ID_MOCK = 'eip155:137/slip44:966'; + describe('submitFiatQuotes', () => { + const buildCaipAssetTypeMock = jest.mocked(buildCaipAssetType); + const getTokenInfoMock = jest.mocked(getTokenInfo); const deriveFiatAssetForFiatPaymentMock = jest.mocked( deriveFiatAssetForFiatPayment, ); @@ -238,6 +242,8 @@ describe('submitFiatQuotes', () => { jest.resetAllMocks(); jest.useRealTimers(); + buildCaipAssetTypeMock.mockReturnValue(FIAT_ASSET_CAIP_ID_MOCK); + getTokenInfoMock.mockReturnValue({ decimals: 18, symbol: 'POL' }); deriveFiatAssetForFiatPaymentMock.mockReturnValue(FIAT_ASSET_MOCK); getRelayQuotesMock.mockResolvedValue([RELAY_QUOTE_RESULT_MOCK]); submitRelayQuotesMock.mockResolvedValue({ @@ -249,7 +255,7 @@ describe('submitFiatQuotes', () => { const order = getFiatOrderMock({ cryptoAmount: '1.2345', cryptoCurrency: { - assetId: FIAT_ASSET_MOCK.caipAssetId, + assetId: FIAT_ASSET_CAIP_ID_MOCK, chainId: 'eip155:137', symbol: 'POL', }, @@ -463,12 +469,12 @@ describe('submitFiatQuotes', () => { dateNowSpy.mockRestore(); }); - it('throws if fiat asset mapping is missing', async () => { - deriveFiatAssetForFiatPaymentMock.mockReturnValue(undefined); + it('throws if token info is unavailable for the fiat asset', async () => { + getTokenInfoMock.mockReturnValue(undefined); const { request } = getRequest(); await expect(submitFiatQuotes(request)).rejects.toThrow( - 'Missing fiat asset mapping for transaction type: predictDeposit', + `Unable to resolve token info for fiat asset ${FIAT_ASSET_MOCK.address} on chain ${FIAT_ASSET_MOCK.chainId}`, ); }); @@ -483,7 +489,7 @@ describe('submitFiatQuotes', () => { }); await expect(submitFiatQuotes(request)).rejects.toThrow( - `Fiat order asset mismatch for transaction ${TRANSACTION_ID_MOCK}: expected ${FIAT_ASSET_MOCK.caipAssetId}, got eip155:137/slip44:60`, + `Fiat order asset mismatch for transaction ${TRANSACTION_ID_MOCK}: expected ${FIAT_ASSET_CAIP_ID_MOCK.toLowerCase()}, got eip155:137/slip44:60`, ); }); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts index 94c2234428..f611e6cf19 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/fiat-submit.ts @@ -14,6 +14,7 @@ import type { QuoteRequest, TransactionPayControllerMessenger, } from '../../types'; +import { buildCaipAssetType, getTokenInfo } from '../../utils/token'; import { getRelayQuotes } from '../relay/relay-quotes'; import { submitRelayQuotes } from '../relay/relay-submit'; import type { RelayQuote } from '../relay/types'; @@ -162,7 +163,10 @@ function validateOrderAsset({ transactionId: string; }): void { const orderAssetId = orderCrypto?.assetId?.toLowerCase(); - const expectedAssetId = expectedAsset.caipAssetId.toLowerCase(); + const expectedAssetId = buildCaipAssetType( + expectedAsset.chainId, + expectedAsset.address, + ).toLowerCase(); const expectedChainId = expectedAssetId.split('/')[0]; const orderChainId = orderCrypto?.chainId?.toLowerCase(); @@ -321,12 +325,7 @@ async function submitRelayAfterFiatCompletion({ throw new Error('Multiple fiat quotes are not supported for submission'); } - const fiatAsset = deriveFiatAssetForFiatPayment(transaction); - if (!fiatAsset) { - throw new Error( - `Missing fiat asset mapping for transaction type: ${String(transaction.type)}`, - ); - } + const fiatAsset = deriveFiatAssetForFiatPayment(transaction, messenger); validateOrderAsset({ expectedAsset: fiatAsset, @@ -334,9 +333,21 @@ async function submitRelayAfterFiatCompletion({ transactionId, }); + const tokenInfo = getTokenInfo( + messenger, + fiatAsset.address, + fiatAsset.chainId, + ); + + if (!tokenInfo) { + throw new Error( + `Unable to resolve token info for fiat asset ${fiatAsset.address} on chain ${fiatAsset.chainId}`, + ); + } + const sourceAmountRaw = getRawSourceAmountFromOrder({ cryptoAmount: order.cryptoAmount, - decimals: fiatAsset.decimals, + decimals: tokenInfo.decimals, }); const baseRequest = quotes[0].request; diff --git a/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts b/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts index 1175bccd56..5f91a94114 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/utils.test.ts @@ -1,44 +1,169 @@ import type { TransactionMeta } from '@metamask/transaction-controller'; import { TransactionType } from '@metamask/transaction-controller'; -import { FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; +import { getDefaultRemoteFeatureFlagControllerState } from '../../../../remote-feature-flag-controller/src/remote-feature-flag-controller'; +import { getMessengerMock } from '../../tests/messenger-mock'; +import { ETH_MAINNET_FIAT_ASSET, FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; +import type { TransactionPayFiatAsset } from './constants'; import { deriveFiatAssetForFiatPayment } from './utils'; +const FEATURE_FLAG_ASSET_MOCK: TransactionPayFiatAsset = { + address: '0x0000000000000000000000000000000000000abc', + chainId: '0xa', +}; + describe('Fiat Utils', () => { + const { messenger, getRemoteFeatureFlagControllerStateMock } = + getMessengerMock(); + + beforeEach(() => { + jest.resetAllMocks(); + + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + }); + }); + describe('deriveFiatAssetForFiatPayment', () => { - it('returns mapped fiat asset for direct transaction type', () => { + it('returns asset from feature flag when present', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.predictDeposit]: FEATURE_FLAG_ASSET_MOCK, + }, + }, + }, + }); + + const transaction = { + type: TransactionType.predictDeposit, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual(FEATURE_FLAG_ASSET_MOCK); + }); + + it('returns feature flag asset over hardcoded asset when both exist', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.predictDeposit]: FEATURE_FLAG_ASSET_MOCK, + }, + }, + }, + }); + const transaction = { type: TransactionType.predictDeposit, } as TransactionMeta; - const result = deriveFiatAssetForFiatPayment(transaction); + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual(FEATURE_FLAG_ASSET_MOCK); + expect(result).not.toStrictEqual( + FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.predictDeposit], + ); + }); + + it('returns hardcoded asset when feature flag has no entry for the type', () => { + const transaction = { + type: TransactionType.predictDeposit, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); expect(result).toStrictEqual( FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.predictDeposit], ); }); - it('returns mapped fiat asset for first nested transaction in batch', () => { + it('returns hardcoded asset for direct transaction type', () => { + const transaction = { + type: TransactionType.perpsDeposit, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual( + FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.perpsDeposit], + ); + }); + + it('returns hardcoded asset for supported nested transaction in batch', () => { const transaction = { nestedTransactions: [{ type: TransactionType.perpsDeposit }], type: TransactionType.batch, } as TransactionMeta; - const result = deriveFiatAssetForFiatPayment(transaction); + const result = deriveFiatAssetForFiatPayment(transaction, messenger); expect(result).toStrictEqual( FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.perpsDeposit], ); }); - it('returns undefined for unsupported type', () => { + it('skips unsupported nested types and finds supported one in batch', () => { + const transaction = { + nestedTransactions: [ + { type: TransactionType.tokenMethodApprove }, + { type: TransactionType.perpsDeposit }, + ], + type: TransactionType.batch, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual( + FIAT_ASSET_ID_BY_TX_TYPE[TransactionType.perpsDeposit], + ); + }); + + it('returns feature flag asset for supported nested transaction in batch', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.perpsDeposit]: FEATURE_FLAG_ASSET_MOCK, + }, + }, + }, + }); + + const transaction = { + nestedTransactions: [{ type: TransactionType.perpsDeposit }], + type: TransactionType.batch, + } as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual(FEATURE_FLAG_ASSET_MOCK); + }); + + it('returns ETH mainnet fallback for unsupported type', () => { const transaction = { type: TransactionType.contractInteraction, } as TransactionMeta; - const result = deriveFiatAssetForFiatPayment(transaction); + const result = deriveFiatAssetForFiatPayment(transaction, messenger); + + expect(result).toStrictEqual(ETH_MAINNET_FIAT_ASSET); + }); + + it('returns ETH mainnet fallback for batch with no nested transactions', () => { + const transaction = { + nestedTransactions: [], + type: TransactionType.batch, + } as unknown as TransactionMeta; + + const result = deriveFiatAssetForFiatPayment(transaction, messenger); - expect(result).toBeUndefined(); + expect(result).toStrictEqual(ETH_MAINNET_FIAT_ASSET); }); }); }); diff --git a/packages/transaction-pay-controller/src/strategy/fiat/utils.ts b/packages/transaction-pay-controller/src/strategy/fiat/utils.ts index 6894127b13..8759473531 100644 --- a/packages/transaction-pay-controller/src/strategy/fiat/utils.ts +++ b/packages/transaction-pay-controller/src/strategy/fiat/utils.ts @@ -1,21 +1,28 @@ -import { - TransactionMeta, - TransactionType, -} from '@metamask/transaction-controller'; +import type { TransactionMeta } from '@metamask/transaction-controller'; +import { TransactionType } from '@metamask/transaction-controller'; -import { FIAT_ASSET_ID_BY_TX_TYPE, TransactionPayFiatAsset } from './constants'; +import type { TransactionPayControllerMessenger } from '../../types'; +import { getFiatAssetPerTransactionType } from '../../utils/feature-flags'; +import type { TransactionPayFiatAsset } from './constants'; +import { FIAT_ASSET_ID_BY_TX_TYPE } from './constants'; export function deriveFiatAssetForFiatPayment( transaction: TransactionMeta, -): TransactionPayFiatAsset | undefined { - const transactionType = transaction?.type; + messenger: TransactionPayControllerMessenger, +): TransactionPayFiatAsset { + const txType = resolveTransactionType(transaction); - if (transactionType === TransactionType.batch) { - const firstMatchingType = transaction.nestedTransactions?.[0]?.type; - if (firstMatchingType) { - return FIAT_ASSET_ID_BY_TX_TYPE[firstMatchingType]; - } + return getFiatAssetPerTransactionType(messenger, txType); +} + +function resolveTransactionType( + transaction: TransactionMeta, +): TransactionType | undefined { + if (transaction.type !== TransactionType.batch) { + return transaction.type; } - return FIAT_ASSET_ID_BY_TX_TYPE[transactionType as TransactionType]; + return transaction.nestedTransactions?.find( + (tx) => tx.type && FIAT_ASSET_ID_BY_TX_TYPE[tx.type] !== undefined, + )?.type; } diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts index 2257127c07..d2455f787d 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.test.ts @@ -1,7 +1,9 @@ +import { TransactionType } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { getDefaultRemoteFeatureFlagControllerState } from '../../../remote-feature-flag-controller/src/remote-feature-flag-controller'; import { TransactionPayStrategy } from '../constants'; +import type { TransactionPayFiatAsset } from '../strategy/fiat/constants'; import { getMessengerMock } from '../tests/messenger-mock'; import { DEFAULT_ACROSS_API_BASE, @@ -13,6 +15,7 @@ import { DEFAULT_SLIPPAGE, getAssetsUnifyStateFeature, getFallbackGas, + getFiatAssetPerTransactionType, DEFAULT_RELAY_EXECUTE_URL, getRelayOriginGasOverhead, getRelayPollingInterval, @@ -1211,4 +1214,105 @@ describe('Feature Flags Utils', () => { expect(getStrategy(messenger)).toBeUndefined(); }); }); + + describe('getFiatAssetPerTransactionType', () => { + const FIAT_ASSET_MOCK: TransactionPayFiatAsset = { + address: '0x0000000000000000000000000000000000001010', + chainId: '0x89', + }; + + it('returns ETH mainnet fallback when confirmations_pay_fiat flag is absent', () => { + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.contractInteraction, + ); + + expect(result).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + chainId: '0x1', + }); + }); + + it('returns hardcoded asset when flag exists but has no entry for the transaction type', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.perpsDeposit]: FIAT_ASSET_MOCK, + }, + }, + }, + }); + + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.predictDeposit, + ); + + expect(result).toStrictEqual({ + address: '0x0000000000000000000000000000000000001010', + chainId: '0x89', + }); + }); + + it('returns feature flag asset when entry matches the transaction type', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.predictDeposit]: FIAT_ASSET_MOCK, + }, + }, + }, + }); + + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.predictDeposit, + ); + + expect(result).toStrictEqual(FIAT_ASSET_MOCK); + }); + + it('returns ETH mainnet fallback when assetPerTransactionType is not defined', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: {}, + }, + }); + + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.contractInteraction, + ); + + expect(result).toStrictEqual({ + address: '0x0000000000000000000000000000000000000000', + chainId: '0x1', + }); + }); + + it('prefers feature flag over hardcoded asset', () => { + getRemoteFeatureFlagControllerStateMock.mockReturnValue({ + ...getDefaultRemoteFeatureFlagControllerState(), + remoteFeatureFlags: { + confirmations_pay_fiat: { + assetPerTransactionType: { + [TransactionType.predictDeposit]: FIAT_ASSET_MOCK, + }, + }, + }, + }); + + const result = getFiatAssetPerTransactionType( + messenger, + TransactionType.predictDeposit, + ); + + expect(result).toStrictEqual(FIAT_ASSET_MOCK); + }); + }); }); diff --git a/packages/transaction-pay-controller/src/utils/feature-flags.ts b/packages/transaction-pay-controller/src/utils/feature-flags.ts index 491853bfdf..f3b3144325 100644 --- a/packages/transaction-pay-controller/src/utils/feature-flags.ts +++ b/packages/transaction-pay-controller/src/utils/feature-flags.ts @@ -1,9 +1,15 @@ +import type { TransactionType } from '@metamask/transaction-controller'; import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { uniq } from 'lodash'; import { isTransactionPayStrategy, TransactionPayStrategy } from '../constants'; import { projectLogger } from '../logger'; +import type { TransactionPayFiatAsset } from '../strategy/fiat/constants'; +import { + ETH_MAINNET_FIAT_ASSET, + FIAT_ASSET_ID_BY_TX_TYPE, +} from '../strategy/fiat/constants'; import { RELAY_EXECUTE_URL, RELAY_POLLING_INTERVAL, @@ -75,6 +81,12 @@ type StrategyOverrides = { transactionTypes: Record; }; +type FiatFlags = { + assetPerTransactionType?: Partial< + Record + >; +}; + type StrategyRoutingConfig = { payStrategies: { across: { @@ -668,6 +680,38 @@ function getCaseInsensitive( return entry?.[1]; } +/** + * Get the fiat asset for a specific transaction type. + * + * Resolution order: + * 1. Feature flag override (`confirmations_pay_fiat.assetPerTransactionType`) + * 2. Hardcoded constant (`FIAT_ASSET_ID_BY_TX_TYPE`) + * 3. ETH mainnet fallback + * + * @param messenger - Controller messenger. + * @param transactionType - Transaction type to look up. + * @returns The fiat asset for the given transaction type. + */ +export function getFiatAssetPerTransactionType( + messenger: TransactionPayControllerMessenger, + transactionType?: TransactionType, +): TransactionPayFiatAsset { + if (!transactionType) { + return ETH_MAINNET_FIAT_ASSET; + } + + const state = messenger.call('RemoteFeatureFlagController:getState'); + const fiatFlags = state.remoteFeatureFlags?.confirmations_pay_fiat as + | FiatFlags + | undefined; + + return ( + fiatFlags?.assetPerTransactionType?.[transactionType] ?? + FIAT_ASSET_ID_BY_TX_TYPE[transactionType] ?? + ETH_MAINNET_FIAT_ASSET + ); +} + /** * Checks if a chain supports EIP-7702. * diff --git a/packages/transaction-pay-controller/src/utils/token.test.ts b/packages/transaction-pay-controller/src/utils/token.test.ts index 0e5cdc9343..bb129d43b8 100644 --- a/packages/transaction-pay-controller/src/utils/token.test.ts +++ b/packages/transaction-pay-controller/src/utils/token.test.ts @@ -13,6 +13,7 @@ import { } from '../constants'; import { getMessengerMock } from '../tests/messenger-mock'; import { + buildCaipAssetType, computeRawFromFiatAmount, computeTokenAmounts, getTokenBalance, @@ -838,4 +839,42 @@ describe('Token Utils', () => { expect(isSameToken(token1, token2)).toBe(false); }); }); + + describe('buildCaipAssetType', () => { + it('returns slip44 asset type for native token on mainnet', () => { + expect(buildCaipAssetType('0x1' as Hex, NATIVE_TOKEN_ADDRESS)).toBe( + 'eip155:1/slip44:60', + ); + }); + + it('returns slip44 asset type for Polygon native token with auto-mapped coin type', () => { + const polygonNative = '0x0000000000000000000000000000000000001010' as Hex; + + expect(buildCaipAssetType('0x89' as Hex, polygonNative)).toBe( + 'eip155:137/slip44:966', + ); + }); + + it('returns slip44 asset type with explicit coin type override', () => { + const polygonNative = '0x0000000000000000000000000000000000001010' as Hex; + + expect(buildCaipAssetType('0x89' as Hex, polygonNative, 966)).toBe( + 'eip155:137/slip44:966', + ); + }); + + it('returns erc20 asset type for ERC-20 token', () => { + const usdcAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Hex; + + expect(buildCaipAssetType('0x1' as Hex, usdcAddress)).toBe( + `eip155:1/erc20:${usdcAddress}`, + ); + }); + + it('defaults slip44CoinType to 60 for native tokens', () => { + expect(buildCaipAssetType('0xa4b1' as Hex, NATIVE_TOKEN_ADDRESS)).toBe( + 'eip155:42161/slip44:60', + ); + }); + }); }); diff --git a/packages/transaction-pay-controller/src/utils/token.ts b/packages/transaction-pay-controller/src/utils/token.ts index a0a2c40b96..b7d4fde67c 100644 --- a/packages/transaction-pay-controller/src/utils/token.ts +++ b/packages/transaction-pay-controller/src/utils/token.ts @@ -3,12 +3,14 @@ import { Web3Provider } from '@ethersproject/providers'; import { TokensControllerState } from '@metamask/assets-controllers'; import { toChecksumHexAddress } from '@metamask/controller-utils'; import { abiERC20 } from '@metamask/metamask-eth-abis'; -import type { Hex } from '@metamask/utils'; +import type { CaipAssetType, Hex } from '@metamask/utils'; +import { hexToBigInt, toCaipAssetType } from '@metamask/utils'; import { BigNumber } from 'bignumber.js'; import { CHAIN_ID_POLYGON, NATIVE_TOKEN_ADDRESS, + SLIP44_COIN_TYPE_BY_CHAIN, STABLECOINS, } from '../constants'; import type { FiatRates, TransactionPayControllerMessenger } from '../types'; @@ -340,6 +342,41 @@ export async function getLiveTokenBalance( return balance.toString(); } +/** + * Build a CAIP-19 asset type identifier for an EVM token. + * + * For native tokens the SLIP-44 coin type is resolved automatically from + * a built-in chain→coin-type map, falling back to 60 (ETH). Callers can + * override via the optional third parameter. + * + * @param chainId - Hex chain ID (e.g. `0x1`). + * @param tokenAddress - Token contract address, or the native token address. + * @param slip44CoinType - Optional SLIP-44 coin type override for native tokens. + * @returns CAIP-19 asset type string. + */ +export function buildCaipAssetType( + chainId: Hex, + tokenAddress: Hex, + slip44CoinType?: number, +): CaipAssetType { + const chainReference = String(hexToBigInt(chainId)); + const isNative = + tokenAddress.toLowerCase() === getNativeToken(chainId).toLowerCase(); + + if (isNative) { + const coinType = slip44CoinType ?? SLIP44_COIN_TYPE_BY_CHAIN[chainId] ?? 60; + + return toCaipAssetType( + 'eip155', + chainReference, + 'slip44', + String(coinType), + ); + } + + return toCaipAssetType('eip155', chainReference, 'erc20', tokenAddress); +} + function getTicker( chainId: Hex, messenger: TransactionPayControllerMessenger, From 4acab9d6d5d0bcdcad36f38bab6119818319e50e Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 8 May 2026 15:00:10 +0200 Subject: [PATCH 5/6] Release 970.0.0 (#8746) ## Explanation Feature release for `json-rpc-engine` introducing a legacy version of `createOriginMiddleware`. ## References ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Low Risk** > Primarily a coordinated version/dependency bump to `@metamask/json-rpc-engine@10.4.0` plus changelog updates; no runtime logic changes in this diff beyond pulling in the new engine release. > > **Overview** > Bumps the monorepo release version to `970.0.0` and publishes `@metamask/json-rpc-engine` as `10.4.0` (changelog notes the addition of the legacy `createOriginMiddleware`). > > Updates dependent packages to require `@metamask/json-rpc-engine@^10.4.0`, refreshes related `CHANGELOG.md` entries, and updates `yarn.lock` accordingly. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1c24a4b77b08346913333c986453755b6f25ee5a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- package.json | 4 +-- packages/base-controller/package.json | 2 +- packages/composable-controller/package.json | 2 +- .../CHANGELOG.md | 1 + .../package.json | 2 +- packages/eth-block-tracker/package.json | 2 +- packages/eth-json-rpc-middleware/CHANGELOG.md | 2 +- packages/eth-json-rpc-middleware/package.json | 2 +- packages/eth-json-rpc-provider/CHANGELOG.md | 2 +- packages/eth-json-rpc-provider/package.json | 2 +- packages/json-rpc-engine/CHANGELOG.md | 5 +++- packages/json-rpc-engine/package.json | 2 +- .../json-rpc-middleware-stream/CHANGELOG.md | 2 +- .../json-rpc-middleware-stream/package.json | 2 +- .../multichain-api-middleware/CHANGELOG.md | 1 + .../multichain-api-middleware/package.json | 2 +- packages/network-controller/CHANGELOG.md | 2 +- packages/network-controller/package.json | 2 +- packages/permission-controller/CHANGELOG.md | 4 +++ packages/permission-controller/package.json | 2 +- .../permission-log-controller/CHANGELOG.md | 2 +- .../permission-log-controller/package.json | 2 +- .../selected-network-controller/CHANGELOG.md | 1 + .../selected-network-controller/package.json | 2 +- yarn.lock | 28 +++++++++---------- 25 files changed, 45 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index a538b91dda..a9662561f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "969.0.0", + "version": "970.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { @@ -54,7 +54,7 @@ "@metamask/eslint-config-typescript": "^15.0.0", "@metamask/eth-block-tracker": "^15.0.1", "@metamask/eth-json-rpc-provider": "^6.0.1", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/network-controller": "^30.1.0", "@metamask/utils": "^11.9.0", "@ts-bridge/cli": "^0.6.4", diff --git a/packages/base-controller/package.json b/packages/base-controller/package.json index cecd1233b8..e76234d00f 100644 --- a/packages/base-controller/package.json +++ b/packages/base-controller/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^6.1.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "deepmerge": "^4.2.2", diff --git a/packages/composable-controller/package.json b/packages/composable-controller/package.json index 8df1542253..fe7dfba754 100644 --- a/packages/composable-controller/package.json +++ b/packages/composable-controller/package.json @@ -56,7 +56,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^6.1.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "deepmerge": "^4.2.2", diff --git a/packages/eip1193-permission-middleware/CHANGELOG.md b/packages/eip1193-permission-middleware/CHANGELOG.md index a8d0da5e4f..0771e1fa2f 100644 --- a/packages/eip1193-permission-middleware/CHANGELOG.md +++ b/packages/eip1193-permission-middleware/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/permission-controller` from `^13.0.0` to `^13.1.0` ([#8722](https://github.com/MetaMask/core/pull/8722)) +- Bump `@metamask/json-rpc-engine` from `^10.3.0` to `^10.4.0` ([#8746](https://github.com/MetaMask/core/pull/8746)) ## [2.0.0] diff --git a/packages/eip1193-permission-middleware/package.json b/packages/eip1193-permission-middleware/package.json index c6dd267663..18a462b1ce 100644 --- a/packages/eip1193-permission-middleware/package.json +++ b/packages/eip1193-permission-middleware/package.json @@ -53,7 +53,7 @@ "dependencies": { "@metamask/chain-agnostic-permission": "^1.5.0", "@metamask/controller-utils": "^11.20.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/permission-controller": "^13.1.0", "@metamask/utils": "^11.9.0", "lodash": "^4.17.21" diff --git a/packages/eth-block-tracker/package.json b/packages/eth-block-tracker/package.json index fc7543566d..8873e6ac6b 100644 --- a/packages/eth-block-tracker/package.json +++ b/packages/eth-block-tracker/package.json @@ -65,7 +65,7 @@ }, "devDependencies": { "@metamask/auto-changelog": "^6.1.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@ts-bridge/cli": "^0.6.4", "@types/jest": "^29.5.14", "@types/json-rpc-random-id": "^1.0.1", diff --git a/packages/eth-json-rpc-middleware/CHANGELOG.md b/packages/eth-json-rpc-middleware/CHANGELOG.md index 2fad210f2c..432d214160 100644 --- a/packages/eth-json-rpc-middleware/CHANGELOG.md +++ b/packages/eth-json-rpc-middleware/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `@metamask/json-rpc-engine` from `^10.2.4` to `^10.3.0` ([#8661](https://github.com/MetaMask/core/pull/8661)) +- Bump `@metamask/json-rpc-engine` from `^10.2.4` to `^10.4.0` ([#8661](https://github.com/MetaMask/core/pull/8661), [#8746](https://github.com/MetaMask/core/pull/8746)) ## [23.1.3] diff --git a/packages/eth-json-rpc-middleware/package.json b/packages/eth-json-rpc-middleware/package.json index e0723c06e5..7789be5650 100644 --- a/packages/eth-json-rpc-middleware/package.json +++ b/packages/eth-json-rpc-middleware/package.json @@ -60,7 +60,7 @@ "@metamask/eth-block-tracker": "^15.0.1", "@metamask/eth-json-rpc-provider": "^6.0.1", "@metamask/eth-sig-util": "^8.2.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/message-manager": "^14.1.1", "@metamask/rpc-errors": "^7.0.2", "@metamask/superstruct": "^3.1.0", diff --git a/packages/eth-json-rpc-provider/CHANGELOG.md b/packages/eth-json-rpc-provider/CHANGELOG.md index bef660bb0a..c240198bb6 100644 --- a/packages/eth-json-rpc-provider/CHANGELOG.md +++ b/packages/eth-json-rpc-provider/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `@metamask/json-rpc-engine` from `^10.2.4` to `^10.3.0` ([#8661](https://github.com/MetaMask/core/pull/8661)) +- Bump `@metamask/json-rpc-engine` from `^10.2.4` to `^10.4.0` ([#8661](https://github.com/MetaMask/core/pull/8661), [#8746](https://github.com/MetaMask/core/pull/8746)) ## [6.0.1] diff --git a/packages/eth-json-rpc-provider/package.json b/packages/eth-json-rpc-provider/package.json index af0c0846a1..9d8c583749 100644 --- a/packages/eth-json-rpc-provider/package.json +++ b/packages/eth-json-rpc-provider/package.json @@ -56,7 +56,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/rpc-errors": "^7.0.2", "@metamask/utils": "^11.9.0", "nanoid": "^3.3.8" diff --git a/packages/json-rpc-engine/CHANGELOG.md b/packages/json-rpc-engine/CHANGELOG.md index 0716a6270b..0bfcc18c0d 100644 --- a/packages/json-rpc-engine/CHANGELOG.md +++ b/packages/json-rpc-engine/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [10.4.0] + ### Added - Add legacy `createOriginMiddleware` utility ([#8734](https://github.com/MetaMask/core/pull/8734)) @@ -302,7 +304,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 This change may affect consumers that depend on the eager execution of middleware _during_ request processing, _outside of_ middleware functions and request handlers. - In general, it is a bad practice to work with state that depends on middleware execution, while the middleware are executing. -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@10.3.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@10.4.0...HEAD +[10.4.0]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@10.3.0...@metamask/json-rpc-engine@10.4.0 [10.3.0]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@10.2.4...@metamask/json-rpc-engine@10.3.0 [10.2.4]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@10.2.3...@metamask/json-rpc-engine@10.2.4 [10.2.3]: https://github.com/MetaMask/core/compare/@metamask/json-rpc-engine@10.2.2...@metamask/json-rpc-engine@10.2.3 diff --git a/packages/json-rpc-engine/package.json b/packages/json-rpc-engine/package.json index 3ee2d7852d..f3abec0df9 100644 --- a/packages/json-rpc-engine/package.json +++ b/packages/json-rpc-engine/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/json-rpc-engine", - "version": "10.3.0", + "version": "10.4.0", "description": "A tool for processing JSON-RPC messages", "keywords": [ "Ethereum", diff --git a/packages/json-rpc-middleware-stream/CHANGELOG.md b/packages/json-rpc-middleware-stream/CHANGELOG.md index 4c38c60b40..2cf9725b55 100644 --- a/packages/json-rpc-middleware-stream/CHANGELOG.md +++ b/packages/json-rpc-middleware-stream/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Upgrade `@metamask/utils` from `^11.8.1` to `^11.9.0` ([#7511](https://github.com/MetaMask/core/pull/7511)) -- Bump `@metamask/json-rpc-engine` from `^10.1.1` to `^10.3.0` ([#7202](https://github.com/MetaMask/core/pull/7202), [#7642](https://github.com/MetaMask/core/pull/7642), [#7856](https://github.com/MetaMask/core/pull/7856), [#8078](https://github.com/MetaMask/core/pull/8078), [#8317](https://github.com/MetaMask/core/pull/8317), [#8661](https://github.com/MetaMask/core/pull/8661)) +- Bump `@metamask/json-rpc-engine` from `^10.1.1` to `^10.4.0` ([#7202](https://github.com/MetaMask/core/pull/7202), [#7642](https://github.com/MetaMask/core/pull/7642), [#7856](https://github.com/MetaMask/core/pull/7856), [#8078](https://github.com/MetaMask/core/pull/8078), [#8317](https://github.com/MetaMask/core/pull/8317), [#8661](https://github.com/MetaMask/core/pull/8661), [#8746](https://github.com/MetaMask/core/pull/8746)) ## [8.0.8] diff --git a/packages/json-rpc-middleware-stream/package.json b/packages/json-rpc-middleware-stream/package.json index 8dd2abea7c..4bcb35f957 100644 --- a/packages/json-rpc-middleware-stream/package.json +++ b/packages/json-rpc-middleware-stream/package.json @@ -51,7 +51,7 @@ "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, "dependencies": { - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/safe-event-emitter": "^3.0.0", "@metamask/utils": "^11.9.0", "readable-stream": "^3.6.2" diff --git a/packages/multichain-api-middleware/CHANGELOG.md b/packages/multichain-api-middleware/CHANGELOG.md index 48aaa75fc4..1acacae09d 100644 --- a/packages/multichain-api-middleware/CHANGELOG.md +++ b/packages/multichain-api-middleware/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bump `@metamask/multichain-transactions-controller` from `^7.0.4` to `^7.1.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) - Bump `@metamask/accounts-controller` from `^37.2.0` to `^38.0.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) - Bump `@metamask/permission-controller` from `^13.0.0` to `^13.1.0` ([#8722](https://github.com/MetaMask/core/pull/8722)) +- Bump `@metamask/json-rpc-engine` from `^10.3.0` to `^10.4.0` ([#8746](https://github.com/MetaMask/core/pull/8746)) ## [3.0.0] diff --git a/packages/multichain-api-middleware/package.json b/packages/multichain-api-middleware/package.json index 3aec0e8776..ba094d70d4 100644 --- a/packages/multichain-api-middleware/package.json +++ b/packages/multichain-api-middleware/package.json @@ -55,7 +55,7 @@ "@metamask/api-specs": "^0.14.0", "@metamask/chain-agnostic-permission": "^1.5.0", "@metamask/controller-utils": "^11.20.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/network-controller": "^30.1.0", "@metamask/permission-controller": "^13.1.0", "@metamask/rpc-errors": "^7.0.2", diff --git a/packages/network-controller/CHANGELOG.md b/packages/network-controller/CHANGELOG.md index a9e536bfc1..f5b98f9791 100644 --- a/packages/network-controller/CHANGELOG.md +++ b/packages/network-controller/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `@metamask/json-rpc-engine` from `^10.2.4` to `^10.3.0` ([#8661](https://github.com/MetaMask/core/pull/8661)) +- Bump `@metamask/json-rpc-engine` from `^10.2.4` to `^10.4.0` ([#8661](https://github.com/MetaMask/core/pull/8661), [#8746](https://github.com/MetaMask/core/pull/8746)) ## [30.1.0] diff --git a/packages/network-controller/package.json b/packages/network-controller/package.json index 7239d2550b..04cc6a9491 100644 --- a/packages/network-controller/package.json +++ b/packages/network-controller/package.json @@ -61,7 +61,7 @@ "@metamask/eth-json-rpc-middleware": "^23.1.3", "@metamask/eth-json-rpc-provider": "^6.0.1", "@metamask/eth-query": "^4.0.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/messenger": "^1.2.0", "@metamask/rpc-errors": "^7.0.2", "@metamask/swappable-obj-proxy": "^2.3.0", diff --git a/packages/permission-controller/CHANGELOG.md b/packages/permission-controller/CHANGELOG.md index da442858b0..ff952519bf 100644 --- a/packages/permission-controller/CHANGELOG.md +++ b/packages/permission-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Bump `@metamask/json-rpc-engine` from `^10.3.0` to `^10.4.0` ([#8746](https://github.com/MetaMask/core/pull/8746)) + ## [13.1.0] ### Added diff --git a/packages/permission-controller/package.json b/packages/permission-controller/package.json index 7122ab066f..1451fad931 100644 --- a/packages/permission-controller/package.json +++ b/packages/permission-controller/package.json @@ -56,7 +56,7 @@ "@metamask/approval-controller": "^9.0.1", "@metamask/base-controller": "^9.1.0", "@metamask/controller-utils": "^11.20.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/messenger": "^1.2.0", "@metamask/rpc-errors": "^7.0.2", "@metamask/utils": "^11.9.0", diff --git a/packages/permission-log-controller/CHANGELOG.md b/packages/permission-log-controller/CHANGELOG.md index fdbd065c32..c3abecced0 100644 --- a/packages/permission-log-controller/CHANGELOG.md +++ b/packages/permission-log-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Bump `@metamask/json-rpc-engine` from `^10.2.4` to `^10.3.0` ([#8661](https://github.com/MetaMask/core/pull/8661)) +- Bump `@metamask/json-rpc-engine` from `^10.2.4` to `^10.4.0` ([#8661](https://github.com/MetaMask/core/pull/8661), [#8746](https://github.com/MetaMask/core/pull/8746)) - Bump `@metamask/messenger` from `^1.0.0` to `^1.2.0` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373), [#8632](https://github.com/MetaMask/core/pull/8632)) - Bump `@metamask/base-controller` from `^9.0.1` to `^9.1.0` ([#8457](https://github.com/MetaMask/core/pull/8457)) diff --git a/packages/permission-log-controller/package.json b/packages/permission-log-controller/package.json index b506a46fd9..e1e3f96c15 100644 --- a/packages/permission-log-controller/package.json +++ b/packages/permission-log-controller/package.json @@ -54,7 +54,7 @@ }, "dependencies": { "@metamask/base-controller": "^9.1.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/messenger": "^1.2.0", "@metamask/utils": "^11.9.0" }, diff --git a/packages/selected-network-controller/CHANGELOG.md b/packages/selected-network-controller/CHANGELOG.md index e4370df253..6597c5184a 100644 --- a/packages/selected-network-controller/CHANGELOG.md +++ b/packages/selected-network-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Bump `@metamask/permission-controller` from `^13.0.0` to `^13.1.0` ([#8722](https://github.com/MetaMask/core/pull/8722)) +- Bump `@metamask/json-rpc-engine` from `^10.3.0` to `^10.4.0` ([#8746](https://github.com/MetaMask/core/pull/8746)) ## [26.1.1] diff --git a/packages/selected-network-controller/package.json b/packages/selected-network-controller/package.json index 137dab8953..6a4421242e 100644 --- a/packages/selected-network-controller/package.json +++ b/packages/selected-network-controller/package.json @@ -54,7 +54,7 @@ }, "dependencies": { "@metamask/base-controller": "^9.1.0", - "@metamask/json-rpc-engine": "^10.3.0", + "@metamask/json-rpc-engine": "^10.4.0", "@metamask/messenger": "^1.2.0", "@metamask/network-controller": "^30.1.0", "@metamask/permission-controller": "^13.1.0", diff --git a/yarn.lock b/yarn.lock index 3f95b15205..6d9d837a54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2977,7 +2977,7 @@ __metadata: resolution: "@metamask/base-controller@workspace:packages/base-controller" dependencies: "@metamask/auto-changelog": "npm:^6.1.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" @@ -3244,7 +3244,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/messenger": "npm:^1.2.0" "@ts-bridge/cli": "npm:^0.6.4" "@types/jest": "npm:^29.5.14" @@ -3386,7 +3386,7 @@ __metadata: "@metamask/eslint-config-typescript": "npm:^15.0.0" "@metamask/eth-block-tracker": "npm:^15.0.1" "@metamask/eth-json-rpc-provider": "npm:^6.0.1" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/network-controller": "npm:^30.1.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" @@ -3578,7 +3578,7 @@ __metadata: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/chain-agnostic-permission": "npm:^1.5.0" "@metamask/controller-utils": "npm:^11.20.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/permission-controller": "npm:^13.1.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/utils": "npm:^11.9.0" @@ -3687,7 +3687,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/eth-json-rpc-provider": "npm:^6.0.1" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" @@ -3755,7 +3755,7 @@ __metadata: "@metamask/eth-block-tracker": "npm:^15.0.1" "@metamask/eth-json-rpc-provider": "npm:^6.0.1" "@metamask/eth-sig-util": "npm:^8.2.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/message-manager": "npm:^14.1.1" "@metamask/network-controller": "npm:^30.1.0" "@metamask/rpc-errors": "npm:^7.0.2" @@ -3798,7 +3798,7 @@ __metadata: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-query": "npm:^0.5.3" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" @@ -4151,7 +4151,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.4, @metamask/json-rpc-engine@npm:^10.3.0, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": +"@metamask/json-rpc-engine@npm:^10.0.0, @metamask/json-rpc-engine@npm:^10.0.2, @metamask/json-rpc-engine@npm:^10.1.0, @metamask/json-rpc-engine@npm:^10.1.1, @metamask/json-rpc-engine@npm:^10.2.4, @metamask/json-rpc-engine@npm:^10.4.0, @metamask/json-rpc-engine@workspace:packages/json-rpc-engine": version: 0.0.0-use.local resolution: "@metamask/json-rpc-engine@workspace:packages/json-rpc-engine" dependencies: @@ -4181,7 +4181,7 @@ __metadata: resolution: "@metamask/json-rpc-middleware-stream@workspace:packages/json-rpc-middleware-stream" dependencies: "@metamask/auto-changelog": "npm:^6.1.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/safe-event-emitter": "npm:^3.0.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" @@ -4594,7 +4594,7 @@ __metadata: "@metamask/chain-agnostic-permission": "npm:^1.5.0" "@metamask/controller-utils": "npm:^11.20.0" "@metamask/eth-json-rpc-filters": "npm:^9.0.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/multichain-transactions-controller": "npm:^7.1.0" "@metamask/network-controller": "npm:^30.1.0" "@metamask/permission-controller": "npm:^13.1.0" @@ -4717,7 +4717,7 @@ __metadata: "@metamask/eth-json-rpc-middleware": "npm:^23.1.3" "@metamask/eth-json-rpc-provider": "npm:^6.0.1" "@metamask/eth-query": "npm:^4.0.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/swappable-obj-proxy": "npm:^2.3.0" @@ -4900,7 +4900,7 @@ __metadata: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/controller-utils": "npm:^11.20.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/utils": "npm:^11.9.0" @@ -4926,7 +4926,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" @@ -5338,7 +5338,7 @@ __metadata: dependencies: "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/json-rpc-engine": "npm:^10.3.0" + "@metamask/json-rpc-engine": "npm:^10.4.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/network-controller": "npm:^30.1.0" "@metamask/permission-controller": "npm:^13.1.0" From fa2dfef3500e8d68318ae76b21c756acd556b11c Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 8 May 2026 16:03:13 +0200 Subject: [PATCH 6/6] feat(json-rpc-engine): Export `assertExpectedHooks` (#8747) ## Explanation Exports the `assertExpectedHooks` utility which is helpful when creating middlewares using `selectHooks`. ## References ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them --- > [!NOTE] > **Low Risk** > Low risk: this is a surface-area change that only adds a new named export and updates tests/changelog, with no behavioral changes to request handling. > > **Overview** > Exposes the `assertExpectedHooks` helper as a public export from `@metamask/json-rpc-engine/v2` (re-exported from `utils`). > > Updates the v2 export snapshot test to include the new symbol and notes the addition in the package changelog. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d0b1873d5941e16d64c5736d4a35e6d6ff4e864c. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/json-rpc-engine/CHANGELOG.md | 4 ++++ packages/json-rpc-engine/src/v2/index.test.ts | 1 + packages/json-rpc-engine/src/v2/index.ts | 1 + 3 files changed, 6 insertions(+) diff --git a/packages/json-rpc-engine/CHANGELOG.md b/packages/json-rpc-engine/CHANGELOG.md index 0bfcc18c0d..93c7679081 100644 --- a/packages/json-rpc-engine/CHANGELOG.md +++ b/packages/json-rpc-engine/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Export `assertExpectedHooks` utility ([#8747](https://github.com/MetaMask/core/pull/8747)) + ## [10.4.0] ### Added diff --git a/packages/json-rpc-engine/src/v2/index.test.ts b/packages/json-rpc-engine/src/v2/index.test.ts index a1e16b5ba6..43a989a1f8 100644 --- a/packages/json-rpc-engine/src/v2/index.test.ts +++ b/packages/json-rpc-engine/src/v2/index.test.ts @@ -9,6 +9,7 @@ describe('@metamask/json-rpc-engine/v2', () => { "JsonRpcServer", "MiddlewareContext", "asLegacyMiddleware", + "assertExpectedHooks", "createMethodMiddleware", "createOriginMiddleware", "createScaffoldMiddleware", diff --git a/packages/json-rpc-engine/src/v2/index.ts b/packages/json-rpc-engine/src/v2/index.ts index ba83b5158e..be91113a34 100644 --- a/packages/json-rpc-engine/src/v2/index.ts +++ b/packages/json-rpc-engine/src/v2/index.ts @@ -23,6 +23,7 @@ export { isRequest, JsonRpcEngineError, selectHooks, + assertExpectedHooks, } from './utils'; export type { Json,