Skip to content

feat: add rate limiting support to controllers leveraging the accounts API #6066

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

mathieuartu
Copy link
Contributor

@mathieuartu mathieuartu commented Jul 3, 2025

Explanation

@MetaMask/identity recently deployed a profileId based (with IP fallback) rate limiting service for the Accounts API.
This PR adds the correct Authentication headers to everywhere in the code the Accounts API was called.

It modifies three controllers, and adds @metamask/profile-sync-controller to their respective package.json file as a peerDependency.

The three controllers are:

  • TokenDetectionController
  • MultichainNetworkController
  • TransactionController

Those are breaking changes, since we need clients to make changes in order to accommodate for this new logic.
An extension test-drive PR can be found below, and act as a reference on how to adapt client code for those breaking changes:

References

Changelog

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, highlighting breaking changes as necessary
  • I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes

@mathieuartu mathieuartu self-assigned this Jul 3, 2025
@mathieuartu mathieuartu added the team-identity Identity Team changes. https://github.com/orgs/MetaMask/teams/identity label Jul 3, 2025
@mathieuartu
Copy link
Contributor Author

@metamaskbot publish-preview

Copy link
Contributor

github-actions bot commented Jul 3, 2025

Preview builds have been published. See these instructions for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/account-tree-controller": "0.4.0-preview-d9822daf",
  "@metamask-previews/accounts-controller": "31.0.0-preview-d9822daf",
  "@metamask-previews/address-book-controller": "6.1.0-preview-d9822daf",
  "@metamask-previews/announcement-controller": "7.0.3-preview-d9822daf",
  "@metamask-previews/app-metadata-controller": "1.0.0-preview-d9822daf",
  "@metamask-previews/approval-controller": "7.1.3-preview-d9822daf",
  "@metamask-previews/assets-controllers": "70.0.0-preview-d9822daf",
  "@metamask-previews/base-controller": "8.0.1-preview-d9822daf",
  "@metamask-previews/bridge-controller": "34.0.0-preview-d9822daf",
  "@metamask-previews/bridge-status-controller": "34.0.0-preview-d9822daf",
  "@metamask-previews/build-utils": "3.0.3-preview-d9822daf",
  "@metamask-previews/chain-agnostic-permission": "1.0.0-preview-d9822daf",
  "@metamask-previews/composable-controller": "11.0.0-preview-d9822daf",
  "@metamask-previews/controller-utils": "11.10.0-preview-d9822daf",
  "@metamask-previews/delegation-controller": "0.5.0-preview-d9822daf",
  "@metamask-previews/earn-controller": "2.0.1-preview-d9822daf",
  "@metamask-previews/eip1193-permission-middleware": "1.0.0-preview-d9822daf",
  "@metamask-previews/ens-controller": "17.0.0-preview-d9822daf",
  "@metamask-previews/error-reporting-service": "2.0.0-preview-d9822daf",
  "@metamask-previews/eth-json-rpc-provider": "4.1.8-preview-d9822daf",
  "@metamask-previews/foundryup": "1.0.0-preview-d9822daf",
  "@metamask-previews/gas-fee-controller": "24.0.0-preview-d9822daf",
  "@metamask-previews/json-rpc-engine": "10.0.3-preview-d9822daf",
  "@metamask-previews/json-rpc-middleware-stream": "8.0.7-preview-d9822daf",
  "@metamask-previews/keyring-controller": "22.1.0-preview-d9822daf",
  "@metamask-previews/logging-controller": "6.0.4-preview-d9822daf",
  "@metamask-previews/message-manager": "12.0.1-preview-d9822daf",
  "@metamask-previews/multichain-api-middleware": "1.0.0-preview-d9822daf",
  "@metamask-previews/multichain-network-controller": "0.9.0-preview-d9822daf",
  "@metamask-previews/multichain-transactions-controller": "3.0.0-preview-d9822daf",
  "@metamask-previews/name-controller": "8.0.3-preview-d9822daf",
  "@metamask-previews/network-controller": "24.0.0-preview-d9822daf",
  "@metamask-previews/notification-services-controller": "12.0.0-preview-d9822daf",
  "@metamask-previews/permission-controller": "11.0.6-preview-d9822daf",
  "@metamask-previews/permission-log-controller": "3.0.3-preview-d9822daf",
  "@metamask-previews/phishing-controller": "12.6.0-preview-d9822daf",
  "@metamask-previews/polling-controller": "14.0.0-preview-d9822daf",
  "@metamask-previews/preferences-controller": "18.4.0-preview-d9822daf",
  "@metamask-previews/profile-sync-controller": "19.0.0-preview-d9822daf",
  "@metamask-previews/rate-limit-controller": "6.0.3-preview-d9822daf",
  "@metamask-previews/remote-feature-flag-controller": "1.6.0-preview-d9822daf",
  "@metamask-previews/sample-controllers": "1.0.0-preview-d9822daf",
  "@metamask-previews/seedless-onboarding-controller": "2.0.0-preview-d9822daf",
  "@metamask-previews/selected-network-controller": "23.0.0-preview-d9822daf",
  "@metamask-previews/signature-controller": "31.0.0-preview-d9822daf",
  "@metamask-previews/token-search-discovery-controller": "3.3.0-preview-d9822daf",
  "@metamask-previews/transaction-controller": "58.1.0-preview-d9822daf",
  "@metamask-previews/user-operation-controller": "37.0.0-preview-d9822daf"
}

@mathieuartu mathieuartu marked this pull request as ready for review July 3, 2025 19:58
@mathieuartu mathieuartu requested review from a team as code owners July 3, 2025 19:58
fabiobozzo
fabiobozzo previously approved these changes Jul 8, 2025
Copy link
Contributor

@fabiobozzo fabiobozzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

AuthenticationControllerGetBearerToken['handler']
> {
return await this.messagingSystem.call(
'AuthenticationController:getBearerToken',
Copy link
Member

@matthewwalsh0 matthewwalsh0 Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all these controllers ultimately call the same action / method, why can't this just be the default where needed in a central accounts API service to avoid all these breaking changes and new dependencies?

Maybe in @metamask/utils or @metamask/controller-utils to avoid a new dependency?

Or even a util such as fetchWithBearerToken?

Or could the clients just implement a proxy to inject this token automatically to avoid any core changes at all and decouple all this from the controllers?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great callout. Do we only use the Accounts API in the accounts controller?

Copy link
Member

@matthewwalsh0 matthewwalsh0 Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think multiple controllers have their own partial abstractions over the accounts API.

My fear is if we keep casually adding peer dependencies between the controllers, we make the release process harder, breaking changes more common, and eventually we'll just have one interdependent bundle meaning no single controller can have a major release without it cascading through the others.

Copy link
Contributor Author

@mathieuartu mathieuartu Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewwalsh0 I'm not sure I understand your comment correctly 😄 AFAIK, AccountsController does not make Accounts API requests, but each of those controllers directly do in their own "bubble". They all have their own logic and wrappers around fetch and also often attach their own headers to these requests. This is why we added this logic at this level.

LMK if that clarifies it - or if I completely missed your point!

Copy link
Member

@matthewwalsh0 matthewwalsh0 Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies, I initially mentioned the AccountsController by mistake, but updated it to clarify how we could abstract this complexity in various ways.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, right now it's a lot of duplication and new dependencies on three new controllers, to ultimately do the same logic. So a central mechanism would be great, either client proxy, util, or shared service.

Copy link
Contributor Author

@mathieuartu mathieuartu Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks everyone for your input!
I think I see the pattern here, and I agree. Nobody wants a new peerDep.

It's just unfortunate that I'll need to juggle around 3 different fetch implementations 😅

  • handleFetch for TokenDetectionController
  • successfulFetch for TransactionController
  • fetch: window.fetch.bind(window) for MultichainNetworkController (as seen on extension)

Copy link
Member

@matthewwalsh0 matthewwalsh0 Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we feel about the proxy / hook idea, to automatically inject this from any client request, meaning no controller changes needed at all? And rather the clients themselves are conceptually "authorised" and so any requests they generate are automatically authorised also?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would automatically injecting the token from any client request look like? Why would it mean no controller changes?

Copy link
Contributor

@cryptodev-2s cryptodev-2s Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of modifying individual service/controller functions. We can also use a rule-based fetch interceptor that automatically adds authentication headers to matching URLs.

Note: The interceptor below only works with globalThis.fetch, so it has limitations when used with other HTTP clients. Additionally, it may need to be adapted to support both extension and mobile environments. We should also consider addressing known issues related to differences in polyfill timing, global scope variations, and Metro bundler behavior. The example is only for idea sharing.

// packages/controller-utils/src/fetch-interceptor.ts
type AuthRule = {
  pattern: string | RegExp;
  getToken: () => Promise<string>;
  headerName?: string;
  enabled?: boolean;
};

class FetchInterceptor {
  private authRules: AuthRule[] = [];
  private originalFetch: typeof globalThis.fetch;
  private isSetup = false;

  constructor() {
    this.originalFetch = globalThis.fetch;
  }

  setup() {
    if (this.isSetup) return;
    
    globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
      const url = this.extractUrl(input);
      const matchedRule = this.findMatchingRule(url);
      
      if (matchedRule && matchedRule.enabled !== false) {
        try {
          const token = await matchedRule.getToken();
          const headerName = matchedRule.headerName || 'Authorization';
          const headerValue = headerName === 'Authorization' ? `Bearer ${token}` : token;
          
          init = {
            ...init,
            headers: {
              ...init?.headers,
              [headerName]: headerValue,
            },
          };
        } catch (error) {
          console.warn(`Failed to get auth token for ${url}:`, error);
          // Continue with original request
        }
      }
      
      return this.originalFetch(input, init);
    };
    
    this.isSetup = true;
  }

  addAuthRule(rule: AuthRule) {
    this.authRules.push(rule);
  }

  teardown() {
    if (this.isSetup) {
      globalThis.fetch = this.originalFetch;
      this.isSetup = false;
    }
  }

  private extractUrl(input: RequestInfo | URL): string {
    if (typeof input === 'string') return input;
    if (input instanceof URL) return input.href;
    return input.url || '';
  }

  private findMatchingRule(url: string): AuthRule | null {
    return this.authRules.find(rule => {
      if (typeof rule.pattern === 'string') {
        return url.includes(rule.pattern);
      }
      return rule.pattern.test(url);
    }) || null;
  }
}

export const fetchInterceptor = new FetchInterceptor();

That we can use in the clients side

import { fetchInterceptor } from '@metamask/controller-utils';

export async function initializeControllers() {
  // Initialize auth controller first
  const authController = new AuthenticationController({...});
  
  // Setup authentication rules
  fetchInterceptor.addAuthRule({
    pattern: 'accounts.api.cx.metamask.io',
    getToken: () => authController.getBearerToken(),
  });
  
  // Activate the interceptor
  fetchInterceptor.setup();
  
  // All subsequent fetch calls are automatically authenticated
  const tokenDetectionController = new TokenDetectionController({...});
}

fabiobozzo
fabiobozzo previously approved these changes Jul 8, 2025
@Gudahtt
Copy link
Member

Gudahtt commented Jul 8, 2025

What's the reasoning behind using profileId here rather than metametricsId? Currently all installations have a metametricsId, but some do not have a profileId.

@@ -2616,10 +2617,10 @@ describe('TokenDetectionController', () => {
properties: {
tokens: [`${sampleTokenA.symbol} - ${sampleTokenA.address}`],
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this a mistake? It looks like the line below would still violate this lint rule.

If the rule doesn't apply for some reason, we should ensure the accompanying TODO comment is also removed (as well as this empty line).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autofixOnSave was the culprit here 🕵️ Fixed with 0ce01be

@mathieuartu mathieuartu dismissed stale reviews from fabiobozzo and Prithpal-Sooriya via 0ce01be July 8, 2025 18:40
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we will end up making changes to this PR, but if not, here is a shallow pass.

@@ -879,10 +918,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
properties: {
tokens: eventTokensDetails,
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove this comment too?

Suggested change
// TODO: Either fix this lint violation or explain why it's necessary to ignore.

@@ -879,10 +918,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
properties: {
tokens: eventTokensDetails,
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention

token_standard: ERC20,
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove this comment too?

Suggested change
// TODO: Either fix this lint violation or explain why it's necessary to ignore.

@@ -1009,10 +1049,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
properties: {
tokens: eventTokensDetails,
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove this comment too?

Suggested change
// TODO: Either fix this lint violation or explain why it's necessary to ignore.

@@ -1009,10 +1049,10 @@ export class TokenDetectionController extends StaticIntervalPollingController<To
properties: {
tokens: eventTokensDetails,
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
// eslint-disable-next-line @typescript-eslint/naming-convention

token_standard: ERC20,
// TODO: Either fix this lint violation or explain why it's necessary to ignore.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove this comment too?

Suggested change
// TODO: Either fix this lint violation or explain why it's necessary to ignore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-changelog team-identity Identity Team changes. https://github.com/orgs/MetaMask/teams/identity
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants