From c4d4ad3939e4a5ec30a41030a6372d292613887d Mon Sep 17 00:00:00 2001 From: MJ Kiwi Date: Wed, 6 May 2026 18:14:05 +1200 Subject: [PATCH 1/3] feat(gator-permissions-controller): Added payee rule (#8668) ## Explanation This PR adds `payee` rule decoding to `@metamask/gator-permissions-controller` execution permission decoding. The 7715 permissions snap can now emit payee restrictions for token payment permissions. Core needs to decode those caveats into the existing decoded permission rule shape: ```ts { type: 'payee', data: { addresses: Address[] } } ``` This PR supports the latest caveat encoding from the snap PR: - Native token payees are decoded from `AllowedTargetsEnforcer`. - Multiple native payees are represented directly as multiple allowed targets. - ERC20 token payees are decoded from `AllowedCalldataEnforcer`. - Single ERC20 payee uses the calldata caveat directly. - Multiple ERC20 payees is not supported yet. - Payee caveats require empty args (`0x`). This also removes `LogicalOrWrapperEnforcer` from native token permission optional enforcers, since native multi-payee permissions no longer use it. ## References - Related snap PR: https://github.com/MetaMask/snap-7715-permissions/pull/300 ## Checklist - [x] 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 - [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** > Updates execution-permission decoding and dependency versions; incorrect rule extraction could lead to misinterpreting payee restrictions or supported enforcer sets across multiple permission types. > > **Overview** > Adds **`payee` rule decoding** to execution permission decoding, extracting allowlisted recipient addresses from `AllowedTargetsEnforcer` (native) and `AllowedCalldataEnforcer` (ERC20) caveats and returning them alongside existing decoded `rules`. > > Updates all known permission rules to recognize the relevant payee enforcers (including adding `AllowedTargetsEnforcer` to the checksummed enforcer set) and exports `EXECUTION_PERMISSION_PAYEE_RULE_TYPE` plus a new `PayeeRule` type. > > Bumps `@metamask/delegation-core` to `^2.0.0` and `@metamask/delegation-deployments` to `^1.3.0`, with expanded test coverage for payee/redeemer rule combinations and payee caveat validation (and an explicit exclusion of payee rule extraction for `erc20-token-revocation`). > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit a5c332eb0d8fa010ff114718ba755170275fbab7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: jeffsmale90 <6363749+jeffsmale90@users.noreply.github.com> --- .../gator-permissions-controller/CHANGELOG.md | 1 + .../gator-permissions-controller/package.json | 4 +- .../src/constants.ts | 8 + .../rules/erc20TokenAllowance.ts | 13 +- .../rules/erc20TokenPeriodic.ts | 13 +- .../rules/erc20TokenRevocation.test.ts | 60 +- .../rules/erc20TokenRevocation.ts | 6 + .../rules/erc20TokenStream.ts | 13 +- .../rules/makePermissionRule.test.ts | 606 +++++++++++++++++- .../rules/makePermissionRule.ts | 149 ++++- .../rules/nativeTokenAllowance.ts | 13 +- .../rules/nativeTokenPeriodic.ts | 13 +- .../rules/nativeTokenStream.ts | 13 +- .../src/decodePermission/types.ts | 1 + .../src/decodePermission/utils.test.ts | 47 +- .../src/decodePermission/utils.ts | 6 + .../gator-permissions-controller/src/index.ts | 2 + .../src/payeeRule.ts | 12 + yarn.lock | 20 +- 19 files changed, 962 insertions(+), 38 deletions(-) create mode 100644 packages/gator-permissions-controller/src/payeeRule.ts diff --git a/packages/gator-permissions-controller/CHANGELOG.md b/packages/gator-permissions-controller/CHANGELOG.md index 8296742ad1..629bc3f7e7 100644 --- a/packages/gator-permissions-controller/CHANGELOG.md +++ b/packages/gator-permissions-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `payee` rule to execution permission decoding for all known permission types ([#8668](https://github.com/MetaMask/core/pull/8668)) - Support `RedeemerEnforcer` caveat when decoding execution permissions ([#8537](https://github.com/MetaMask/core/pull/8537)) - Permission decoding now recognizes the `RedeemerEnforcer` as an optional caveat on all execution permission types and extracts a `redeemer` rule containing the allowlisted addresses. - `DecodedPermission` type now includes an optional `rules` property for rules recovered from caveats. diff --git a/packages/gator-permissions-controller/package.json b/packages/gator-permissions-controller/package.json index eff5bae911..117b5086a9 100644 --- a/packages/gator-permissions-controller/package.json +++ b/packages/gator-permissions-controller/package.json @@ -56,8 +56,8 @@ "@metamask/7715-permission-types": "^0.6.0", "@metamask/abi-utils": "^2.0.3", "@metamask/base-controller": "^9.1.0", - "@metamask/delegation-core": "^1.1.0", - "@metamask/delegation-deployments": "^0.12.0", + "@metamask/delegation-core": "^2.0.0", + "@metamask/delegation-deployments": "^1.3.0", "@metamask/messenger": "^1.2.0", "@metamask/network-controller": "^30.1.0", "@metamask/snaps-controllers": "^19.0.0", diff --git a/packages/gator-permissions-controller/src/constants.ts b/packages/gator-permissions-controller/src/constants.ts index feda806752..7a4ad01c57 100644 --- a/packages/gator-permissions-controller/src/constants.ts +++ b/packages/gator-permissions-controller/src/constants.ts @@ -10,3 +10,11 @@ export const DELEGATION_FRAMEWORK_VERSION = '1.3.0'; * supported execution permission type. */ export const EXECUTION_PERMISSION_REDEEMER_RULE_TYPE = 'redeemer' as const; + +/** + * `Rule.type` / `wallet_getSupportedExecutionPermissions` `ruleTypes` entry for + * payee allowlists (AllowedCalldataEnforcer / AllowedTargetsEnforcer). Hosts + * should advertise this for every supported execution permission type that supports + * payee restrictions. + */ +export const EXECUTION_PERMISSION_PAYEE_RULE_TYPE = 'payee' as const; diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts index 47d41de17f..01c5e02ea9 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenAllowance.ts @@ -33,12 +33,23 @@ export function makeErc20TokenAllowanceRule( erc20PeriodicEnforcer, valueLteEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-allowance', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedCalldataEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }, timestampEnforcer, requiredEnforcers: { [erc20PeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts index 7ccd7430e1..5b84141063 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenPeriodic.ts @@ -29,12 +29,23 @@ export function makeErc20TokenPeriodicRule( erc20PeriodicEnforcer, valueLteEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-periodic', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedCalldataEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }, timestampEnforcer, requiredEnforcers: { [erc20PeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts index b27c8b5af4..32d2a37cd2 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.test.ts @@ -3,14 +3,19 @@ import { CHAIN_ID, DELEGATOR_CONTRACTS, } from '@metamask/delegation-deployments'; +import { getChecksumAddress } from '@metamask/utils'; import { createPermissionRulesForContracts } from '.'; describe('erc20-token-revocation rule', () => { const chainId = CHAIN_ID.sepolia; const contracts = DELEGATOR_CONTRACTS['1.3.0'][chainId]; - const { TimestampEnforcer, AllowedCalldataEnforcer, ValueLteEnforcer } = - contracts; + const { + TimestampEnforcer, + AllowedCalldataEnforcer, + ValueLteEnforcer, + RedeemerEnforcer, + } = contracts; const permissionRules = createPermissionRulesForContracts(contracts); const rule = permissionRules.find( (candidate) => candidate.permissionType === 'erc20-token-revocation', @@ -217,5 +222,56 @@ describe('erc20-token-revocation rule', () => { expect(result.expiry).toBe(1720000); expect(result.data).toStrictEqual({}); + expect(result.rules).toBeUndefined(); + }); + + it('includes redeemer rule but not payee when RedeemerEnforcer caveat is present', () => { + const packedAddr = '1111111111111111111111111111111111111111' as const; + const approveSelectorTerms = + '0x0000000000000000000000000000000000000000000000000000000000000000095ea7b3' as const; + const zeroAmountTerms = + '0x00000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000000' as const; + const zeroValueLteTerms = + '0x0000000000000000000000000000000000000000000000000000000000000000' as const; + const caveats = [ + expiryCaveat, + { + enforcer: AllowedCalldataEnforcer, + terms: approveSelectorTerms, + args: '0x' as const, + }, + { + enforcer: AllowedCalldataEnforcer, + terms: zeroAmountTerms, + args: '0x' as const, + }, + { + enforcer: ValueLteEnforcer, + terms: zeroValueLteTerms, + args: '0x' as const, + }, + { + enforcer: RedeemerEnforcer, + terms: `0x${packedAddr}` as const, + args: '0x' as const, + }, + ]; + const result = rule.validateAndDecodePermission(caveats); + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toStrictEqual([ + { + type: 'redeemer', + data: { + addresses: [ + getChecksumAddress( + '0x1111111111111111111111111111111111111111' as const, + ), + ], + }, + }, + ]); }); }); diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts index 8dde558038..a127829d8e 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenRevocation.ts @@ -24,6 +24,7 @@ export function makeErc20TokenRevocationRule( const { timestampEnforcer, allowedCalldataEnforcer, + allowedTargetsEnforcer, valueLteEnforcer, nonceEnforcer, redeemerEnforcer, @@ -32,6 +33,11 @@ export function makeErc20TokenRevocationRule( permissionType: 'erc20-token-revocation', optionalEnforcers: [timestampEnforcer, redeemerEnforcer], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }, timestampEnforcer, requiredEnforcers: { [allowedCalldataEnforcer]: 2, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts index ee08c3852f..c87c782173 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/erc20TokenStream.ts @@ -28,12 +28,23 @@ export function makeErc20TokenStreamRule( erc20StreamingEnforcer, valueLteEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'erc20-token-stream', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedCalldataEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }, timestampEnforcer, requiredEnforcers: { [erc20StreamingEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts index 37a7906840..fef9a97124 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.test.ts @@ -1,4 +1,8 @@ -import { createTimestampTerms } from '@metamask/delegation-core'; +import { + createAllowedCalldataTerms, + createAllowedTargetsTerms, + createTimestampTerms, +} from '@metamask/delegation-core'; import { CHAIN_ID, DELEGATOR_CONTRACTS, @@ -13,6 +17,20 @@ describe('makePermissionRule', () => { const timestampEnforcer = contracts.TimestampEnforcer; const requiredEnforcer = contracts.NonceEnforcer; const redeemerEnforcer = contracts.RedeemerEnforcer; + const allowedCalldataEnforcer = contracts.AllowedCalldataEnforcer; + const allowedTargetsEnforcer = contracts.AllowedTargetsEnforcer; + + const payeeEnforcersNative = { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedTargetsEnforcer, + }; + + const payeeEnforcersErc20 = { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedCalldataEnforcer, + }; it('calls optional validate callback when provided and decoding succeeds', () => { const validateAndDecodeData = jest.fn().mockReturnValue({}); @@ -21,6 +39,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -60,6 +79,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -95,6 +115,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -131,6 +152,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -169,6 +191,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -208,6 +231,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -239,6 +263,7 @@ describe('makePermissionRule', () => { permissionType: 'native-token-stream', timestampEnforcer, redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, optionalEnforcers: [], requiredEnforcers: { [requiredEnforcer]: 1 }, validateAndDecodeData, @@ -276,4 +301,583 @@ describe('makePermissionRule', () => { }, ]); }); + + it('includes payee rule when AllowedTargetsEnforcer caveat is present (native)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress] }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toStrictEqual([ + { + type: 'payee', + data: { + addresses: [getChecksumAddress(payeeAddress)], + }, + }, + ]); + }); + + it('includes payee rule when AllowedCalldataEnforcer caveat is present (erc20)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x3333333333333333333333333333333333333333' as Hex; + const paddedAddress = `0x${payeeAddress.slice(2).padStart(64, '0')}`; + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: paddedAddress, + }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toStrictEqual([ + { + type: 'payee', + data: { + addresses: [getChecksumAddress(payeeAddress)], + }, + }, + ]); + }); + + it('does not include payee rule when only AllowedTargetsEnforcer caveat is present (erc20)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress] }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toBeUndefined(); + }); + + it('rejects multiple AllowedCalldataEnforcer caveats for erc20 payee decoding', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress1 = '0x2222222222222222222222222222222222222222' as Hex; + const payeeAddress2 = '0x3333333333333333333333333333333333333333' as Hex; + const padded1 = `0x${payeeAddress1.slice(2).padStart(64, '0')}`; + const padded2 = `0x${payeeAddress2.slice(2).padStart(64, '0')}`; + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: padded1, + }), + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: padded2, + }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + if (result.isValid) { + throw new Error('Expected invalid result'); + } + expect(result.error.message).toBe( + 'Invalid payee caveats: multiple singlePayeeEnforcer caveats', + ); + }); + + it('includes payee rule with multiple addresses via AllowedTargetsEnforcer (native)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress1 = '0x4444444444444444444444444444444444444444' as Hex; + const payeeAddress2 = '0x5555555555555555555555555555555555555555' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ + targets: [payeeAddress1, payeeAddress2], + }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toStrictEqual([ + { + type: 'payee', + data: { + addresses: [ + getChecksumAddress(payeeAddress1), + getChecksumAddress(payeeAddress2), + ], + }, + }, + ]); + }); + + it('does not include payee rule when only AllowedCalldataEnforcer caveat is present (native)', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x3333333333333333333333333333333333333333' as Hex; + const paddedAddress = `0x${payeeAddress.slice(2).padStart(64, '0')}`; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: paddedAddress, + }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toBeUndefined(); + }); + + it('includes both redeemer and payee rules when both caveats present', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const redeemerAddr = '1111111111111111111111111111111111111111' as const; + const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [redeemerEnforcer, allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: redeemerEnforcer, + terms: `0x${redeemerAddr}` as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress] }), + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toHaveLength(2); + expect(result.rules).toStrictEqual([ + { + type: 'redeemer', + data: { + addresses: [getChecksumAddress(`0x${redeemerAddr}` as Hex)], + }, + }, + { + type: 'payee', + data: { + addresses: [getChecksumAddress(payeeAddress)], + }, + }, + ]); + }); + + it('does not include payee rule when no payee caveat is present', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(true); + if (!result.isValid) { + throw new Error('Expected valid result'); + } + expect(result.rules).toBeUndefined(); + }); + + it('returns true from caveatAddressesMatch when enforcers match rule', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [timestampEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + expect( + rule.caveatAddressesMatch([requiredEnforcer, timestampEnforcer]), + ).toBe(true); + expect(rule.caveatAddressesMatch([requiredEnforcer])).toBe(true); + expect(rule.caveatAddressesMatch([])).toBe(false); + }); + + it('rejects when singlePayeeEnforcer is unrecognised', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const unknownEnforcer = '0x8888888888888888888888888888888888888888' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: unknownEnforcer, + }, + optionalEnforcers: [unknownEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const caveats = [ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: unknownEnforcer, + terms: + '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex, + args: '0x' as Hex, + }, + ]; + + const result = rule.validateAndDecodePermission(caveats); + + expect(result.isValid).toBe(false); + }); + + it('rejects when singlePayeeEnforcer is configured as a required enforcer', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x2222222222222222222222222222222222222222' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [], + requiredEnforcers: { + [requiredEnforcer]: 1, + [allowedTargetsEnforcer]: 1, + }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress] }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + if (result.isValid) { + throw new Error('Expected invalid result'); + } + expect(result.error.message).toBe( + 'Invalid payee caveats: singlePayeeEnforcer may not be a required caveat', + ); + }); + + it('rejects an ERC20 payee caveat with the wrong calldata start index', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress = '0x3333333333333333333333333333333333333333' as const; + const paddedAddress = + `0x${payeeAddress.slice(2).padStart(64, '0')}` as const; + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 36, + value: paddedAddress, + }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + }); + + it('rejects an ERC20 payee caveat when the calldata value is not one address', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + + const rule = makePermissionRule({ + permissionType: 'erc20-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersErc20, + optionalEnforcers: [allowedCalldataEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedCalldataEnforcer, + terms: createAllowedCalldataTerms({ + startIndex: 4, + value: '0x1234', + }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + }); + + it('rejects a native payee caveat with no targets', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + }); + + it('rejects multiple single-payee caveats', () => { + const validateAndDecodeData = jest.fn().mockReturnValue({}); + const payeeAddress1 = '0x2222222222222222222222222222222222222222' as Hex; + const payeeAddress2 = '0x3333333333333333333333333333333333333333' as Hex; + + const rule = makePermissionRule({ + permissionType: 'native-token-stream', + timestampEnforcer, + redeemerEnforcer, + payeeEnforcers: payeeEnforcersNative, + optionalEnforcers: [allowedTargetsEnforcer], + requiredEnforcers: { [requiredEnforcer]: 1 }, + validateAndDecodeData, + }); + + const result = rule.validateAndDecodePermission([ + { + enforcer: requiredEnforcer, + terms: '0x' as Hex, + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress1] }), + args: '0x' as Hex, + }, + { + enforcer: allowedTargetsEnforcer, + terms: createAllowedTargetsTerms({ targets: [payeeAddress2] }), + args: '0x' as Hex, + }, + ]); + + expect(result.isValid).toBe(false); + }); }); diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts index a2cefb1f21..b3966908fa 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/makePermissionRule.ts @@ -1,10 +1,17 @@ import type { Rule } from '@metamask/7715-permission-types'; import type { Caveat } from '@metamask/delegation-core'; -import { decodeRedeemerTerms } from '@metamask/delegation-core'; +import { + decodeAllowedCalldataTerms, + decodeAllowedTargetsTerms, + decodeRedeemerTerms, +} from '@metamask/delegation-core'; import { getChecksumAddress, isStrictHexString } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; -import { EXECUTION_PERMISSION_REDEEMER_RULE_TYPE } from '../../constants'; +import { + EXECUTION_PERMISSION_PAYEE_RULE_TYPE, + EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, +} from '../../constants'; import type { ChecksumCaveat, DecodedPermission, @@ -16,9 +23,19 @@ import { buildEnforcerCountsAndSet, enforcersMatchRule, extractExpiryFromCaveatTerms, + getByteLength, getTermsByEnforcer, } from '../utils'; +const ERC20_TRANSFER_PAYEE_START_INDEX = 4; +const ERC20_PAYEE_VALUE_BYTE_LENGTH = 32; + +type PayeeEnforcerAddresses = { + allowedCalldataEnforcer: Hex; + allowedTargetsEnforcer: Hex; + singlePayeeEnforcer: Hex; +}; + /** * Creates a single permission rule with the given type, enforcer sets, and * decode/validate callbacks. @@ -26,6 +43,7 @@ import { * @param args - The arguments to this function. * @param args.optionalEnforcers - Enforcer addresses that may appear in addition to required. * @param args.redeemerEnforcer - Address of the RedeemerEnforcer used to extract redeemer rules. + * @param args.payeeEnforcers - Addresses of enforcers used to extract payee rules. * @param args.timestampEnforcer - Address of the TimestampEnforcer used to extract expiry. * @param args.permissionType - The permission type identifier. * @param args.requiredEnforcers - Map of required enforcer address to required count. @@ -35,6 +53,7 @@ import { export function makePermissionRule({ optionalEnforcers, redeemerEnforcer, + payeeEnforcers, timestampEnforcer, permissionType, requiredEnforcers, @@ -42,6 +61,7 @@ export function makePermissionRule({ }: { optionalEnforcers: Hex[]; redeemerEnforcer: Hex; + payeeEnforcers: PayeeEnforcerAddresses; timestampEnforcer: Hex; permissionType: PermissionType; requiredEnforcers: Record; @@ -106,22 +126,129 @@ export function makePermissionRule({ throwIfNotFound: false, }); - let rules: Rule[] | undefined; + const rules: Rule[] = []; if (redeemerTerms) { - rules = [ - { - type: EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, - data: { - addresses: decodeRedeemerTerms(redeemerTerms).redeemers, - }, + rules.push({ + type: EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, + data: { + addresses: decodeRedeemerTerms(redeemerTerms).redeemers, }, - ]; + }); } - return { isValid: true, expiry, data, rules }; + // todo: this is a temporary fix to exclude payee rules from erc20-token-revocation + // a nicer solution may be to pass an array of permissionRule decoders to the makePermissionRule + // function. + if (permissionType !== 'erc20-token-revocation') { + const payeeAddresses = tryExtractPayeeAddresses( + checksumCaveats, + payeeEnforcers, + requiredEnforcersMap, + ); + if (payeeAddresses) { + rules.push({ + type: EXECUTION_PERMISSION_PAYEE_RULE_TYPE, + data: { addresses: payeeAddresses }, + }); + } + } + + return { + isValid: true, + expiry, + data, + rules: rules.length > 0 ? rules : undefined, + }; } catch (caughtError) { return { isValid: false, error: caughtError as Error }; } }, }; } + +/** + * Attempts to extract payee addresses from a payee enforcer caveat. + * + * @param caveat - The payee caveat to decode. + * @param payeeEnforcerAddresses - Known payee enforcer addresses for comparison. + * @param payeeEnforcerAddresses.allowedCalldataEnforcer - AllowedCalldataEnforcer address. + * @param payeeEnforcerAddresses.allowedTargetsEnforcer - AllowedTargetsEnforcer address. + * @returns The checksummed payee addresses, or null if the enforcer is unrecognised. + */ +function extractPayeeAddressesFromCaveat( + caveat: Caveat, + payeeEnforcerAddresses: { + allowedCalldataEnforcer: Hex; + allowedTargetsEnforcer: Hex; + }, +): Hex[] { + const checksumEnforcer = getChecksumAddress(caveat.enforcer); + + if (checksumEnforcer === payeeEnforcerAddresses.allowedCalldataEnforcer) { + const decoded = decodeAllowedCalldataTerms(caveat.terms); + if (decoded.startIndex !== ERC20_TRANSFER_PAYEE_START_INDEX) { + throw new Error( + `Invalid payee caveat: AllowedCalldataEnforcer startIndex must be ${ERC20_TRANSFER_PAYEE_START_INDEX}`, + ); + } + + if (getByteLength(decoded.value) !== ERC20_PAYEE_VALUE_BYTE_LENGTH) { + throw new Error( + `Invalid payee caveat: AllowedCalldataEnforcer value must be ${ERC20_PAYEE_VALUE_BYTE_LENGTH} bytes long`, + ); + } + + const address: Hex = `0x${decoded.value.slice(-40)}`; + return [getChecksumAddress(address)]; + } + + if (checksumEnforcer === payeeEnforcerAddresses.allowedTargetsEnforcer) { + const decoded = decodeAllowedTargetsTerms(caveat.terms); + return decoded.targets.map(getChecksumAddress); + } + + throw new Error('Invalid payee caveat: unrecognised enforcer'); +} + +/** + * Attempts to extract payee addresses from caveats, handling both single-payee + * (direct enforcer) and multi-payee (RedeemerEnforcer). + * + * @param caveats - Checksummed caveats from the delegation. + * @param enforcers - Payee enforcer addresses. + * @param enforcers.allowedCalldataEnforcer - AllowedCalldataEnforcer address. + * @param enforcers.allowedTargetsEnforcer - AllowedTargetsEnforcer address. + * @param enforcers.singlePayeeEnforcer - The specific enforcer for single-payee in this permission type. + * @param requiredEnforcers - Required enforcer counts for the permission rule. + * @returns Array of checksummed payee addresses, or null if no payee caveat is found. + */ +function tryExtractPayeeAddresses( + caveats: ChecksumCaveat[], + enforcers: PayeeEnforcerAddresses, + requiredEnforcers: Map, +): Hex[] | null { + if (requiredEnforcers.has(enforcers.singlePayeeEnforcer)) { + throw new Error( + 'Invalid payee caveats: singlePayeeEnforcer may not be a required caveat', + ); + } + + const singlePayeeCaveats = caveats.filter( + (caveat) => caveat.enforcer === enforcers.singlePayeeEnforcer, + ); + + // this should not be possible, unless the singlePayeeCaveat is also included for a different rule, for the permission itself + if (singlePayeeCaveats.length > 1) { + throw new Error( + 'Invalid payee caveats: multiple singlePayeeEnforcer caveats', + ); + } + + const singlePayeeCaveat = singlePayeeCaveats[0] ?? null; + + if (singlePayeeCaveat) { + return extractPayeeAddressesFromCaveat(singlePayeeCaveat, enforcers); + } + + return null; +} diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts index 4ead98866c..9f5910cc1b 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenAllowance.ts @@ -33,12 +33,23 @@ export function makeNativeTokenAllowanceRule( nativeTokenPeriodicEnforcer, exactCalldataEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-allowance', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedTargetsEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedTargetsEnforcer, + }, timestampEnforcer, requiredEnforcers: { [nativeTokenPeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts index a86a020219..cb1a0c3d29 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenPeriodic.ts @@ -28,12 +28,23 @@ export function makeNativeTokenPeriodicRule( nativeTokenPeriodicEnforcer, exactCalldataEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-periodic', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedTargetsEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedTargetsEnforcer, + }, timestampEnforcer, requiredEnforcers: { [nativeTokenPeriodicEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts index 4c19a036dc..57454b3929 100644 --- a/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts +++ b/packages/gator-permissions-controller/src/decodePermission/rules/nativeTokenStream.ts @@ -23,12 +23,23 @@ export function makeNativeTokenStreamRule( nativeTokenStreamingEnforcer, exactCalldataEnforcer, nonceEnforcer, + allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = enforcers; return makePermissionRule({ permissionType: 'native-token-stream', - optionalEnforcers: [timestampEnforcer, redeemerEnforcer], + optionalEnforcers: [ + timestampEnforcer, + redeemerEnforcer, + allowedTargetsEnforcer, + ], redeemerEnforcer, + payeeEnforcers: { + allowedCalldataEnforcer, + allowedTargetsEnforcer, + singlePayeeEnforcer: allowedTargetsEnforcer, + }, timestampEnforcer, requiredEnforcers: { [nativeTokenStreamingEnforcer]: 1, diff --git a/packages/gator-permissions-controller/src/decodePermission/types.ts b/packages/gator-permissions-controller/src/decodePermission/types.ts index 08ae71abd6..e27d4a8033 100644 --- a/packages/gator-permissions-controller/src/decodePermission/types.ts +++ b/packages/gator-permissions-controller/src/decodePermission/types.ts @@ -99,6 +99,7 @@ export type ChecksumEnforcersByChainId = { timestampEnforcer: Hex; nonceEnforcer: Hex; allowedCalldataEnforcer: Hex; + allowedTargetsEnforcer: Hex; redeemerEnforcer: Hex; }; diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts index 7cf7f43d54..2cc7902c1c 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.test.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.test.ts @@ -22,6 +22,7 @@ const buildContracts = (): DeployedContractsByName => ({ ValueLteEnforcer: '0x7777777777777777777777777777777777777777', NonceEnforcer: '0x8888888888888888888888888888888888888888', AllowedCalldataEnforcer: '0x9999999999999999999999999999999999999999', + AllowedTargetsEnforcer: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', RedeemerEnforcer: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', }); @@ -52,6 +53,9 @@ describe('getChecksumEnforcersByChainId', () => { allowedCalldataEnforcer: getChecksumAddress( contracts.AllowedCalldataEnforcer, ), + allowedTargetsEnforcer: getChecksumAddress( + contracts.AllowedTargetsEnforcer, + ), redeemerEnforcer: getChecksumAddress(contracts.RedeemerEnforcer), }); }); @@ -78,6 +82,7 @@ describe('createPermissionRulesForChainId', () => { timestampEnforcer, nonceEnforcer, allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, } = getChecksumEnforcersByChainId(contracts); @@ -101,13 +106,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-stream'].permissionType).toBe( 'native-token-stream', ); - expect(byType['native-token-stream'].optionalEnforcers.size).toBe(2); + expect(byType['native-token-stream'].optionalEnforcers.size).toBe(3); expect( byType['native-token-stream'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['native-token-stream'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['native-token-stream'].optionalEnforcers.has( + allowedTargetsEnforcer, + ), + ).toBe(true); expect(byType['native-token-stream'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-stream'].requiredEnforcers.entries()), @@ -124,13 +134,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-periodic'].permissionType).toBe( 'native-token-periodic', ); - expect(byType['native-token-periodic'].optionalEnforcers.size).toBe(2); + expect(byType['native-token-periodic'].optionalEnforcers.size).toBe(3); expect( byType['native-token-periodic'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['native-token-periodic'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['native-token-periodic'].optionalEnforcers.has( + allowedTargetsEnforcer, + ), + ).toBe(true); expect(byType['native-token-periodic'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-periodic'].requiredEnforcers.entries()), @@ -147,13 +162,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-stream'].permissionType).toBe( 'erc20-token-stream', ); - expect(byType['erc20-token-stream'].optionalEnforcers.size).toBe(2); + expect(byType['erc20-token-stream'].optionalEnforcers.size).toBe(3); expect( byType['erc20-token-stream'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['erc20-token-stream'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['erc20-token-stream'].optionalEnforcers.has( + allowedCalldataEnforcer, + ), + ).toBe(true); expect(byType['erc20-token-stream'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-stream'].requiredEnforcers.entries()), @@ -170,13 +190,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-periodic'].permissionType).toBe( 'erc20-token-periodic', ); - expect(byType['erc20-token-periodic'].optionalEnforcers.size).toBe(2); + expect(byType['erc20-token-periodic'].optionalEnforcers.size).toBe(3); expect( byType['erc20-token-periodic'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['erc20-token-periodic'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['erc20-token-periodic'].optionalEnforcers.has( + allowedCalldataEnforcer, + ), + ).toBe(true); expect(byType['erc20-token-periodic'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-periodic'].requiredEnforcers.entries()), @@ -193,13 +218,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['native-token-allowance'].permissionType).toBe( 'native-token-allowance', ); - expect(byType['native-token-allowance'].optionalEnforcers.size).toBe(2); + expect(byType['native-token-allowance'].optionalEnforcers.size).toBe(3); expect( byType['native-token-allowance'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['native-token-allowance'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['native-token-allowance'].optionalEnforcers.has( + allowedTargetsEnforcer, + ), + ).toBe(true); expect(byType['native-token-allowance'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['native-token-allowance'].requiredEnforcers.entries()), @@ -216,13 +246,18 @@ describe('createPermissionRulesForChainId', () => { expect(byType['erc20-token-allowance'].permissionType).toBe( 'erc20-token-allowance', ); - expect(byType['erc20-token-allowance'].optionalEnforcers.size).toBe(2); + expect(byType['erc20-token-allowance'].optionalEnforcers.size).toBe(3); expect( byType['erc20-token-allowance'].optionalEnforcers.has(timestampEnforcer), ).toBe(true); expect( byType['erc20-token-allowance'].optionalEnforcers.has(redeemerEnforcer), ).toBe(true); + expect( + byType['erc20-token-allowance'].optionalEnforcers.has( + allowedCalldataEnforcer, + ), + ).toBe(true); expect(byType['erc20-token-allowance'].requiredEnforcers.size).toBe(3); expect( Array.from(byType['erc20-token-allowance'].requiredEnforcers.entries()), diff --git a/packages/gator-permissions-controller/src/decodePermission/utils.ts b/packages/gator-permissions-controller/src/decodePermission/utils.ts index 603dcd6772..4177159c64 100644 --- a/packages/gator-permissions-controller/src/decodePermission/utils.ts +++ b/packages/gator-permissions-controller/src/decodePermission/utils.ts @@ -20,6 +20,7 @@ const ENFORCER_CONTRACT_NAMES = { ValueLteEnforcer: 'ValueLteEnforcer', NonceEnforcer: 'NonceEnforcer', AllowedCalldataEnforcer: 'AllowedCalldataEnforcer', + AllowedTargetsEnforcer: 'AllowedTargetsEnforcer', RedeemerEnforcer: 'RedeemerEnforcer', }; @@ -109,6 +110,10 @@ export const getChecksumEnforcersByChainId = ( ENFORCER_CONTRACT_NAMES.AllowedCalldataEnforcer, ); + const allowedTargetsEnforcer = getChecksumContractAddress( + ENFORCER_CONTRACT_NAMES.AllowedTargetsEnforcer, + ); + const redeemerEnforcer = getChecksumContractAddress( ENFORCER_CONTRACT_NAMES.RedeemerEnforcer, ); @@ -123,6 +128,7 @@ export const getChecksumEnforcersByChainId = ( timestampEnforcer, nonceEnforcer, allowedCalldataEnforcer, + allowedTargetsEnforcer, redeemerEnforcer, }; }; diff --git a/packages/gator-permissions-controller/src/index.ts b/packages/gator-permissions-controller/src/index.ts index 125248cb72..0661ccf966 100644 --- a/packages/gator-permissions-controller/src/index.ts +++ b/packages/gator-permissions-controller/src/index.ts @@ -1,6 +1,7 @@ export { default as GatorPermissionsController } from './GatorPermissionsController'; export { DELEGATION_FRAMEWORK_VERSION, + EXECUTION_PERMISSION_PAYEE_RULE_TYPE, EXECUTION_PERMISSION_REDEEMER_RULE_TYPE, } from './constants'; export type { @@ -37,6 +38,7 @@ export type { SupportedPermissionType, } from './types'; +export type { PayeeRule } from './payeeRule'; export type { RedeemerRule } from './redeemerRule'; export type { NativeTokenStreamPermission, diff --git a/packages/gator-permissions-controller/src/payeeRule.ts b/packages/gator-permissions-controller/src/payeeRule.ts new file mode 100644 index 0000000000..4731c16c35 --- /dev/null +++ b/packages/gator-permissions-controller/src/payeeRule.ts @@ -0,0 +1,12 @@ +import type { Hex } from '@metamask/utils'; + +/** + * Execution permission rule restricting which addresses may receive payments + * (on-chain AllowedCalldataEnforcer / AllowedTargetsEnforcer caveat). + */ +export type PayeeRule = { + type: 'payee'; + data: { + addresses: Hex[]; + }; +}; diff --git a/yarn.lock b/yarn.lock index df7b371804..35fbdb22f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3479,21 +3479,21 @@ __metadata: languageName: unknown linkType: soft -"@metamask/delegation-core@npm:^1.1.0": - version: 1.1.0 - resolution: "@metamask/delegation-core@npm:1.1.0" +"@metamask/delegation-core@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/delegation-core@npm:2.0.0" dependencies: "@metamask/abi-utils": "npm:^3.0.0" "@metamask/utils": "npm:^11.4.0" "@noble/hashes": "npm:^1.8.0" - checksum: 10/672f9e2e2b4e8c312f2cd2ff166bbc508fbdb6e141fe92e678abc9993b9ccbdd17db711477a9b97b6ce3919fa6d51d759c16f6c6fda3f89cb95e303b8aa76f7d + checksum: 10/b473160e4cb4a6d463c6015de6e90d057034d2e8f2905068e1f44f93c8247618c5d84a155e86dfaa125dacb040951643517b9a76961bf8d215c194dc4d1cc0ad languageName: node linkType: hard -"@metamask/delegation-deployments@npm:^0.12.0": - version: 0.12.0 - resolution: "@metamask/delegation-deployments@npm:0.12.0" - checksum: 10/fd3b373efc1857cc867b44b4ca33db0cf8487c1109d6f2ed7e3ce10e6a65d4165b7fcc034cab92d919d6f0833e3749a055ff862adc8d7a348cdd3a0f593f6aa6 +"@metamask/delegation-deployments@npm:^1.3.0": + version: 1.3.0 + resolution: "@metamask/delegation-deployments@npm:1.3.0" + checksum: 10/58f4aafb5f0e3cbc543811cbc0100efab4ed67b9c9794b83192962153e4edbe12fd6ab6fa7be689503309862a65eb7fde771f632893d38ab54f8171aa682b34f languageName: node linkType: hard @@ -4109,8 +4109,8 @@ __metadata: "@metamask/abi-utils": "npm:^2.0.3" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/delegation-core": "npm:^1.1.0" - "@metamask/delegation-deployments": "npm:^0.12.0" + "@metamask/delegation-core": "npm:^2.0.0" + "@metamask/delegation-deployments": "npm:^1.3.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/network-controller": "npm:^30.1.0" "@metamask/snaps-controllers": "npm:^19.0.0" From 8f9effbc10b56236d2b6c6470a0fde673669e7f1 Mon Sep 17 00:00:00 2001 From: infiniteflower <139582705+infiniteflower@users.noreply.github.com> Date: Wed, 6 May 2026 15:38:50 +0900 Subject: [PATCH 2/3] Release/960.0.0 (#8706) ## Explanation This PR releases BridgeController ## 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** > Low risk release bookkeeping: version/changelog updates and dependency bumps only, with no runtime logic changes in this diff. > > **Overview** > **Release bump only.** Updates the monorepo version to `960.0.0` and publishes `@metamask/bridge-controller@71.1.0` (adds changelog entry and bumps the package version). > > Propagates the new `@metamask/bridge-controller` version to downstream packages (`bridge-status-controller` and `transaction-pay-controller`) and updates `yarn.lock` accordingly. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ba9bed3e23d8ac19b67dbd0c8c5d7557d61418f7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Cursor --- package.json | 2 +- packages/bridge-controller/CHANGELOG.md | 5 ++++- packages/bridge-controller/package.json | 2 +- packages/bridge-status-controller/CHANGELOG.md | 1 + packages/bridge-status-controller/package.json | 2 +- packages/transaction-pay-controller/CHANGELOG.md | 1 + packages/transaction-pay-controller/package.json | 2 +- yarn.lock | 6 +++--- 8 files changed, 13 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 4d1ade7419..652ab0d6bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/core-monorepo", - "version": "959.0.0", + "version": "960.0.0", "private": true, "description": "Monorepo for packages shared between MetaMask clients", "repository": { diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index f778e7abd7..6e44cbccdc 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [71.1.0] + ### Added - Add optional `batchSellDestStablecoins` chain-level feature flag to bridge configuration ([#8705](https://github.com/MetaMask/core/pull/8705)) @@ -1407,7 +1409,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release ([#5317](https://github.com/MetaMask/core/pull/5317)) -[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@71.0.0...HEAD +[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@71.1.0...HEAD +[71.1.0]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@71.0.0...@metamask/bridge-controller@71.1.0 [71.0.0]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@70.2.0...@metamask/bridge-controller@71.0.0 [70.2.0]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@70.1.1...@metamask/bridge-controller@70.2.0 [70.1.1]: https://github.com/MetaMask/core/compare/@metamask/bridge-controller@70.1.0...@metamask/bridge-controller@70.1.1 diff --git a/packages/bridge-controller/package.json b/packages/bridge-controller/package.json index ff9591f6e0..82f8122ee0 100644 --- a/packages/bridge-controller/package.json +++ b/packages/bridge-controller/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/bridge-controller", - "version": "71.0.0", + "version": "71.1.0", "description": "Manages bridge-related quote fetching functionality for MetaMask", "keywords": [ "Ethereum", diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 50ff1d3992..c6043dc99f 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Bump `@metamask/bridge-controller` from `^71.0.0` to `^71.1.0` ([#8706](https://github.com/MetaMask/core/pull/8706)) - Bump `@metamask/transaction-controller` from `^65.0.0` to `^65.1.0` ([#8691](https://github.com/MetaMask/core/pull/8691)) - Bump `@metamask/accounts-controller` from `^37.2.0` to `^38.0.0` ([#8665](https://github.com/MetaMask/core/pull/8665)) - Bump `@metamask/messenger` from `^1.1.1` to `^1.2.0` ([#8632](https://github.com/MetaMask/core/pull/8632)) diff --git a/packages/bridge-status-controller/package.json b/packages/bridge-status-controller/package.json index 3e1bdd6c35..1b8ed01e5b 100644 --- a/packages/bridge-status-controller/package.json +++ b/packages/bridge-status-controller/package.json @@ -54,7 +54,7 @@ "dependencies": { "@metamask/accounts-controller": "^38.0.0", "@metamask/base-controller": "^9.1.0", - "@metamask/bridge-controller": "^71.0.0", + "@metamask/bridge-controller": "^71.1.0", "@metamask/controller-utils": "^11.20.0", "@metamask/gas-fee-controller": "^26.1.1", "@metamask/keyring-controller": "^25.4.0", diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index 512cddc57c..b1ea46c656 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Bump `@metamask/bridge-controller` from `^71.0.0` to `^71.1.0` ([#8706](https://github.com/MetaMask/core/pull/8706)) - Bump `@metamask/transaction-controller` from `^65.0.0` to `^65.1.0` ([#8691](https://github.com/MetaMask/core/pull/8691)) - Bump `@metamask/ramps-controller` from `^13.2.0` to `^13.3.0` ([#8698](https://github.com/MetaMask/core/pull/8698)) diff --git a/packages/transaction-pay-controller/package.json b/packages/transaction-pay-controller/package.json index 223f8fa42d..da58bbfbc1 100644 --- a/packages/transaction-pay-controller/package.json +++ b/packages/transaction-pay-controller/package.json @@ -60,7 +60,7 @@ "@metamask/assets-controller": "^6.3.0", "@metamask/assets-controllers": "^105.1.0", "@metamask/base-controller": "^9.1.0", - "@metamask/bridge-controller": "^71.0.0", + "@metamask/bridge-controller": "^71.1.0", "@metamask/bridge-status-controller": "^71.1.0", "@metamask/controller-utils": "^11.20.0", "@metamask/gas-fee-controller": "^26.1.1", diff --git a/yarn.lock b/yarn.lock index 35fbdb22f7..22b95c7789 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3014,7 +3014,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/bridge-controller@npm:^71.0.0, @metamask/bridge-controller@workspace:packages/bridge-controller": +"@metamask/bridge-controller@npm:^71.1.0, @metamask/bridge-controller@workspace:packages/bridge-controller": version: 0.0.0-use.local resolution: "@metamask/bridge-controller@workspace:packages/bridge-controller" dependencies: @@ -3068,7 +3068,7 @@ __metadata: "@metamask/accounts-controller": "npm:^38.0.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/bridge-controller": "npm:^71.0.0" + "@metamask/bridge-controller": "npm:^71.1.0" "@metamask/controller-utils": "npm:^11.20.0" "@metamask/gas-fee-controller": "npm:^26.1.1" "@metamask/keyring-controller": "npm:^25.4.0" @@ -5734,7 +5734,7 @@ __metadata: "@metamask/assets-controllers": "npm:^105.1.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/base-controller": "npm:^9.1.0" - "@metamask/bridge-controller": "npm:^71.0.0" + "@metamask/bridge-controller": "npm:^71.1.0" "@metamask/bridge-status-controller": "npm:^71.1.0" "@metamask/controller-utils": "npm:^11.20.0" "@metamask/gas-fee-controller": "npm:^26.1.1" From a4afaa16fcc3437be9127a5a61c452da6d929375 Mon Sep 17 00:00:00 2001 From: Michele Esposito <34438276+mikesposito@users.noreply.github.com> Date: Wed, 6 May 2026 10:05:37 +0200 Subject: [PATCH 3/3] chore: add missing Infura networks (#8680) ## Explanation Adding networks to `controller-utils` that are included in the dynamic registry but were missing from the package Infura networks list. This addition is necessary to get passed the validation checks in `NetworkController` when adding these networks from the dynamic registry (https://client-config.api.cx.metamask.io/v1/config/networks). ## References ## Checklist - [x] 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 - [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** > Moderate risk because it expands the set of built-in Infura networks (chain IDs, tickers, explorer URLs, nicknames), which can affect network validation and default network configuration behavior for consumers. > > **Overview** > Adds missing Infura-supported mainnet networks to `controller-utils` by extending `InfuraNetworkType` and related constants (`BuiltInNetworkName`, `ChainId`, `NetworksTicker`, `BlockExplorerUrl`, `NetworkNickname`) for `megaeth-mainnet`, `monad-mainnet`, `avalanche-mainnet`, and `zksync-mainnet`. > > Updates `NetworkController` test snapshots/expectations so these networks are included in default state/config and corresponding Infura network clients, and records the change in the `controller-utils` changelog. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit bd05e8d508039056b281cd31d586bafa2ac0a1b9. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- packages/controller-utils/CHANGELOG.md | 1 + packages/controller-utils/src/types.ts | 25 ++ .../tests/NetworkController.test.ts | 360 ++++++++++++++++++ 3 files changed, 386 insertions(+) diff --git a/packages/controller-utils/CHANGELOG.md b/packages/controller-utils/CHANGELOG.md index 7fa784a4bb..31c761889e 100644 --- a/packages/controller-utils/CHANGELOG.md +++ b/packages/controller-utils/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The event still emits a `FailureReason` when retries are exhausted. - Update `normalizeEnsName` regex to allow ENS names with 3 or more characters (previously required 7 or more) ([#8510](https://github.com/MetaMask/core/pull/8510)) - Update default Sei Mainnet block explorer URL from `seitrace.com` to `seiscan.io` ([#8545](https://github.com/MetaMask/core/pull/8545)) +- Update `InfuraNetworkType`, `ChainId`, `NetworksTicker`, `BlockExplorerUrl`, and `NetworkNickname` to include missing Infura networks ([#8680](https://github.com/MetaMask/core/pull/8680)) ## [11.20.0] diff --git a/packages/controller-utils/src/types.ts b/packages/controller-utils/src/types.ts index 93061e4d76..95ac170672 100644 --- a/packages/controller-utils/src/types.ts +++ b/packages/controller-utils/src/types.ts @@ -14,6 +14,10 @@ export const InfuraNetworkType = { 'optimism-mainnet': 'optimism-mainnet', 'polygon-mainnet': 'polygon-mainnet', 'sei-mainnet': 'sei-mainnet', + 'monad-mainnet': 'monad-mainnet', + 'megaeth-mainnet': 'megaeth-mainnet', + 'avalanche-mainnet': 'avalanche-mainnet', + 'zksync-mainnet': 'zksync-mainnet', } as const; export type InfuraNetworkType = @@ -99,6 +103,9 @@ export enum BuiltInNetworkName { PolygonMainnet = 'polygon-mainnet', SeiMainnet = 'sei-mainnet', MegaETHMainnet = 'megaeth-mainnet', + MonadMainnet = 'monad-mainnet', + AvalancheMainnet = 'avalanche-mainnet', + ZksyncMainnet = 'zksync-mainnet', } /** @@ -127,6 +134,9 @@ export const ChainId = { [BuiltInNetworkName.PolygonMainnet]: '0x89', // toHex(137) [BuiltInNetworkName.SeiMainnet]: '0x531', // toHex(1329) [BuiltInNetworkName.MegaETHMainnet]: '0x10e6', // toHex(4326) + [BuiltInNetworkName.MonadMainnet]: '0x8f', // toHex(143) + [BuiltInNetworkName.AvalancheMainnet]: '0xa86a', // toHex(43114) + [BuiltInNetworkName.ZksyncMainnet]: '0x144', // toHex(324) } as const; export type ChainId = (typeof ChainId)[keyof typeof ChainId]; @@ -156,6 +166,13 @@ export enum NetworksTicker { 'optimism-mainnet' = 'ETH', 'polygon-mainnet' = 'POL', 'sei-mainnet' = 'SEI', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + 'megaeth-mainnet' = 'ETH', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + 'monad-mainnet' = 'MON', + 'avalanche-mainnet' = 'AVAX', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + 'zksync-mainnet' = 'ETH', rpc = '', } /* eslint-enable @typescript-eslint/naming-convention */ @@ -180,6 +197,10 @@ export const BlockExplorerUrl = { [BuiltInNetworkName.OptimismMainnet]: 'https://optimistic.etherscan.io', [BuiltInNetworkName.PolygonMainnet]: 'https://polygonscan.com', [BuiltInNetworkName.SeiMainnet]: 'https://seiscan.io', + [BuiltInNetworkName.MegaETHMainnet]: 'https://megaeth.blockscout.com', + [BuiltInNetworkName.MonadMainnet]: 'https://monadscan.com', + [BuiltInNetworkName.AvalancheMainnet]: 'https://snowtrace.io', + [BuiltInNetworkName.ZksyncMainnet]: 'https://explorer.zksync.io', } as const satisfies Record; export type BlockExplorerUrl = (typeof BlockExplorerUrl)[keyof typeof BlockExplorerUrl]; @@ -203,6 +224,10 @@ export const NetworkNickname = { [BuiltInNetworkName.OptimismMainnet]: 'Optimism Mainnet', [BuiltInNetworkName.PolygonMainnet]: 'Polygon Mainnet', [BuiltInNetworkName.SeiMainnet]: 'Sei Mainnet', + [BuiltInNetworkName.MegaETHMainnet]: 'MegaETH Mainnet', + [BuiltInNetworkName.MonadMainnet]: 'Monad Mainnet', + [BuiltInNetworkName.AvalancheMainnet]: 'Avalanche Mainnet', + [BuiltInNetworkName.ZksyncMainnet]: 'ZKsync Era', } as const satisfies Record; export type NetworkNickname = (typeof NetworkNickname)[keyof typeof NetworkNickname]; diff --git a/packages/network-controller/tests/NetworkController.test.ts b/packages/network-controller/tests/NetworkController.test.ts index 80c0f89b0b..c7bddcd60e 100644 --- a/packages/network-controller/tests/NetworkController.test.ts +++ b/packages/network-controller/tests/NetworkController.test.ts @@ -518,6 +518,36 @@ describe('NetworkController', () => { }, ], }, + "0x10e6": { + "blockExplorerUrls": [], + "chainId": "0x10e6", + "defaultRpcEndpointIndex": 0, + "name": "MegaETH Mainnet", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "megaeth-mainnet", + "type": "infura", + "url": "https://megaeth-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, + "0x144": { + "blockExplorerUrls": [], + "chainId": "0x144", + "defaultRpcEndpointIndex": 0, + "name": "ZKsync Era", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "zksync-mainnet", + "type": "infura", + "url": "https://zksync-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0x2105": { "blockExplorerUrls": [], "chainId": "0x2105", @@ -578,6 +608,21 @@ describe('NetworkController', () => { }, ], }, + "0x8f": { + "blockExplorerUrls": [], + "chainId": "0x8f", + "defaultRpcEndpointIndex": 0, + "name": "Monad Mainnet", + "nativeCurrency": "MON", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "monad-mainnet", + "type": "infura", + "url": "https://monad-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xa": { "blockExplorerUrls": [], "chainId": "0xa", @@ -608,6 +653,21 @@ describe('NetworkController', () => { }, ], }, + "0xa86a": { + "blockExplorerUrls": [], + "chainId": "0xa86a", + "defaultRpcEndpointIndex": 0, + "name": "Avalanche Mainnet", + "nativeCurrency": "AVAX", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "avalanche-mainnet", + "type": "infura", + "url": "https://avalanche-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xaa36a7": { "blockExplorerUrls": [], "chainId": "0xaa36a7", @@ -687,6 +747,36 @@ describe('NetworkController', () => { }, ], }, + "0x10e6": { + "blockExplorerUrls": [], + "chainId": "0x10e6", + "defaultRpcEndpointIndex": 0, + "name": "MegaETH Mainnet", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "megaeth-mainnet", + "type": "infura", + "url": "https://megaeth-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, + "0x144": { + "blockExplorerUrls": [], + "chainId": "0x144", + "defaultRpcEndpointIndex": 0, + "name": "ZKsync Era", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "zksync-mainnet", + "type": "infura", + "url": "https://zksync-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0x18c6": { "blockExplorerUrls": [ "https://megaexplorer.xyz", @@ -765,6 +855,21 @@ describe('NetworkController', () => { }, ], }, + "0x8f": { + "blockExplorerUrls": [], + "chainId": "0x8f", + "defaultRpcEndpointIndex": 0, + "name": "Monad Mainnet", + "nativeCurrency": "MON", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "monad-mainnet", + "type": "infura", + "url": "https://monad-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xa": { "blockExplorerUrls": [], "chainId": "0xa", @@ -795,6 +900,21 @@ describe('NetworkController', () => { }, ], }, + "0xa86a": { + "blockExplorerUrls": [], + "chainId": "0xa86a", + "defaultRpcEndpointIndex": 0, + "name": "Avalanche Mainnet", + "nativeCurrency": "AVAX", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "avalanche-mainnet", + "type": "infura", + "url": "https://avalanche-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xaa36a7": { "blockExplorerUrls": [], "chainId": "0xaa36a7", @@ -1951,6 +2071,21 @@ describe('NetworkController', () => { enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), }, + 'avalanche-mainnet': { + blockTracker: expect.anything(), + configuration: { + type: NetworkClientType.Infura, + failoverRpcUrls: [], + infuraProjectId, + chainId: '0xa86a', + ticker: 'AVAX', + network: InfuraNetworkType['avalanche-mainnet'], + }, + provider: expect.anything(), + destroy: expect.any(Function), + enableRpcFailover: expect.any(Function), + disableRpcFailover: expect.any(Function), + }, 'base-mainnet': { blockTracker: expect.anything(), configuration: { @@ -2026,6 +2161,36 @@ describe('NetworkController', () => { enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), }, + 'megaeth-mainnet': { + blockTracker: expect.anything(), + configuration: { + type: NetworkClientType.Infura, + failoverRpcUrls: [], + infuraProjectId, + chainId: '0x10e6', + ticker: 'ETH', + network: InfuraNetworkType['megaeth-mainnet'], + }, + provider: expect.anything(), + destroy: expect.any(Function), + enableRpcFailover: expect.any(Function), + disableRpcFailover: expect.any(Function), + }, + 'monad-mainnet': { + blockTracker: expect.anything(), + configuration: { + type: NetworkClientType.Infura, + failoverRpcUrls: [], + infuraProjectId, + chainId: '0x8f', + ticker: 'MON', + network: InfuraNetworkType['monad-mainnet'], + }, + provider: expect.anything(), + destroy: expect.any(Function), + enableRpcFailover: expect.any(Function), + disableRpcFailover: expect.any(Function), + }, 'optimism-mainnet': { blockTracker: expect.anything(), configuration: { @@ -2086,6 +2251,21 @@ describe('NetworkController', () => { enableRpcFailover: expect.any(Function), disableRpcFailover: expect.any(Function), }, + 'zksync-mainnet': { + blockTracker: expect.anything(), + configuration: { + type: NetworkClientType.Infura, + failoverRpcUrls: [], + infuraProjectId, + chainId: '0x144', + ticker: 'ETH', + network: InfuraNetworkType['zksync-mainnet'], + }, + provider: expect.anything(), + destroy: expect.any(Function), + enableRpcFailover: expect.any(Function), + disableRpcFailover: expect.any(Function), + }, }); }, ); @@ -14910,6 +15090,36 @@ describe('NetworkController', () => { }, ], }, + "0x10e6": { + "blockExplorerUrls": [], + "chainId": "0x10e6", + "defaultRpcEndpointIndex": 0, + "name": "MegaETH Mainnet", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "megaeth-mainnet", + "type": "infura", + "url": "https://megaeth-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, + "0x144": { + "blockExplorerUrls": [], + "chainId": "0x144", + "defaultRpcEndpointIndex": 0, + "name": "ZKsync Era", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "zksync-mainnet", + "type": "infura", + "url": "https://zksync-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0x2105": { "blockExplorerUrls": [], "chainId": "0x2105", @@ -14970,6 +15180,21 @@ describe('NetworkController', () => { }, ], }, + "0x8f": { + "blockExplorerUrls": [], + "chainId": "0x8f", + "defaultRpcEndpointIndex": 0, + "name": "Monad Mainnet", + "nativeCurrency": "MON", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "monad-mainnet", + "type": "infura", + "url": "https://monad-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xa": { "blockExplorerUrls": [], "chainId": "0xa", @@ -15000,6 +15225,21 @@ describe('NetworkController', () => { }, ], }, + "0xa86a": { + "blockExplorerUrls": [], + "chainId": "0xa86a", + "defaultRpcEndpointIndex": 0, + "name": "Avalanche Mainnet", + "nativeCurrency": "AVAX", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "avalanche-mainnet", + "type": "infura", + "url": "https://avalanche-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xaa36a7": { "blockExplorerUrls": [], "chainId": "0xaa36a7", @@ -15079,6 +15319,36 @@ describe('NetworkController', () => { }, ], }, + "0x10e6": { + "blockExplorerUrls": [], + "chainId": "0x10e6", + "defaultRpcEndpointIndex": 0, + "name": "MegaETH Mainnet", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "megaeth-mainnet", + "type": "infura", + "url": "https://megaeth-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, + "0x144": { + "blockExplorerUrls": [], + "chainId": "0x144", + "defaultRpcEndpointIndex": 0, + "name": "ZKsync Era", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "zksync-mainnet", + "type": "infura", + "url": "https://zksync-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0x2105": { "blockExplorerUrls": [], "chainId": "0x2105", @@ -15139,6 +15409,21 @@ describe('NetworkController', () => { }, ], }, + "0x8f": { + "blockExplorerUrls": [], + "chainId": "0x8f", + "defaultRpcEndpointIndex": 0, + "name": "Monad Mainnet", + "nativeCurrency": "MON", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "monad-mainnet", + "type": "infura", + "url": "https://monad-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xa": { "blockExplorerUrls": [], "chainId": "0xa", @@ -15169,6 +15454,21 @@ describe('NetworkController', () => { }, ], }, + "0xa86a": { + "blockExplorerUrls": [], + "chainId": "0xa86a", + "defaultRpcEndpointIndex": 0, + "name": "Avalanche Mainnet", + "nativeCurrency": "AVAX", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "avalanche-mainnet", + "type": "infura", + "url": "https://avalanche-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xaa36a7": { "blockExplorerUrls": [], "chainId": "0xaa36a7", @@ -15248,6 +15548,36 @@ describe('NetworkController', () => { }, ], }, + "0x10e6": { + "blockExplorerUrls": [], + "chainId": "0x10e6", + "defaultRpcEndpointIndex": 0, + "name": "MegaETH Mainnet", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "megaeth-mainnet", + "type": "infura", + "url": "https://megaeth-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, + "0x144": { + "blockExplorerUrls": [], + "chainId": "0x144", + "defaultRpcEndpointIndex": 0, + "name": "ZKsync Era", + "nativeCurrency": "ETH", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "zksync-mainnet", + "type": "infura", + "url": "https://zksync-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0x2105": { "blockExplorerUrls": [], "chainId": "0x2105", @@ -15308,6 +15638,21 @@ describe('NetworkController', () => { }, ], }, + "0x8f": { + "blockExplorerUrls": [], + "chainId": "0x8f", + "defaultRpcEndpointIndex": 0, + "name": "Monad Mainnet", + "nativeCurrency": "MON", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "monad-mainnet", + "type": "infura", + "url": "https://monad-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xa": { "blockExplorerUrls": [], "chainId": "0xa", @@ -15338,6 +15683,21 @@ describe('NetworkController', () => { }, ], }, + "0xa86a": { + "blockExplorerUrls": [], + "chainId": "0xa86a", + "defaultRpcEndpointIndex": 0, + "name": "Avalanche Mainnet", + "nativeCurrency": "AVAX", + "rpcEndpoints": [ + { + "failoverUrls": [], + "networkClientId": "avalanche-mainnet", + "type": "infura", + "url": "https://avalanche-mainnet.infura.io/v3/{infuraProjectId}", + }, + ], + }, "0xaa36a7": { "blockExplorerUrls": [], "chainId": "0xaa36a7",