{"version":3,"file":"CurrencyRateController.cjs","sourceRoot":"","sources":["../src/CurrencyRateController.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAKA,iEAGoC;AAOpC,qEAA+E;AAE/E,6CAAoC;AAGpC,oEAAyE;AAyBzE,MAAM,IAAI,GAAG,wBAAwB,CAAC;AA0BtC,MAAM,QAAQ,GAAqC;IACjD,eAAe,EAAE;QACf,kBAAkB,EAAE,IAAI;QACxB,OAAO,EAAE,IAAI;QACb,sBAAsB,EAAE,IAAI;QAC5B,QAAQ,EAAE,IAAI;KACf;IACD,aAAa,EAAE;QACb,kBAAkB,EAAE,IAAI;QACxB,OAAO,EAAE,IAAI;QACb,sBAAsB,EAAE,IAAI;QAC5B,QAAQ,EAAE,IAAI;KACf;CACF,CAAC;AAEF,MAAM,YAAY,GAAG;IACnB,eAAe,EAAE,KAAK;IACtB,aAAa,EAAE;QACb,GAAG,EAAE;YACH,cAAc,EAAE,CAAC;YACjB,cAAc,EAAE,CAAC;YACjB,iBAAiB,EAAE,IAAI;SACxB;KACF;CACF,CAAC;AAOF,MAAM,sBAAsB,GAAG,CAAC,KAAa,EAAE,SAAS,GAAG,CAAC,EAAU,EAAE,CACtE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;AAcnC;;;GAGG;AACH,MAAa,sBAAuB,SAAQ,IAAA,oDAA+B,GAI1E;IASC;;;;;;;;;;OAUG;IACH,YAAY,EACV,cAAc,GAAG,KAAK,EACtB,QAAQ,GAAG,MAAM,EACjB,mBAAmB,GAAG,GAAG,EAAE,CAAC,IAAI,EAChC,SAAS,EACT,KAAK,EACL,kBAAkB,GAQnB;QACC,KAAK,CAAC;YACJ,IAAI;YACJ,QAAQ;YACR,SAAS;YACT,KAAK,EAAE,EAAE,GAAG,YAAY,EAAE,GAAG,KAAK,EAAE;SACrC,CAAC,CAAC;;QAvCI,wCAAS,IAAI,mBAAK,EAAE,EAAC;QAErB,yDAAyB;QAEzB,8DAAoC;QAEpC,6DAAgD;QAkCvD,uBAAA,IAAI,0CAAmB,cAAc,MAAA,CAAC;QACtC,uBAAA,IAAI,+CAAwB,mBAAmB,MAAA,CAAC;QAChD,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QACjC,uBAAA,IAAI,8CAAuB,kBAAkB,MAAA,CAAC;IAChD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,kBAAkB,CAAC,eAAuB;QAC9C,MAAM,WAAW,GAAG,MAAM,uBAAA,IAAI,qCAAO,CAAC,OAAO,EAAE,CAAC;QAChD,MAAM,gBAAgB,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QAC/D,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;gBACf,OAAO;oBACL,GAAG,YAAY;oBACf,eAAe;iBAChB,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,WAAW,EAAE,CAAC;QAChB,CAAC;QACD,gFAAgF;QAChF,mEAAmE;QACnE,IAAI,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;IAC5C,CAAC;IA4ND;;;;OAIG;IACH,KAAK,CAAC,kBAAkB,CACtB,gBAAwC;QAExC,IAAI,CAAC,uBAAA,IAAI,mDAAqB,MAAzB,IAAI,CAAuB,EAAE,CAAC;YACjC,OAAO;QACT,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,uBAAA,IAAI,qCAAO,CAAC,OAAO,EAAE,CAAC;QAChD,IAAI,CAAC;YACH,wFAAwF;YACxF,kEAAkE;YAClE,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,yCAAsB,CAAC,CAAC;YAC7D,MAAM,uBAAuB,GAAG,gBAAgB,CAAC,MAAM,CAErD,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE;gBACxB,IAAI,CAAC,cAAc,EAAE,CAAC;oBACpB,OAAO,GAAG,CAAC;gBACb,CAAC;gBAED,GAAG,CAAC,cAAc,CAAC,GAAG,cAAc,CAAC,QAAQ,CAAC,cAAc,CAAC;oBAC3D,CAAC,CAAC,wCAAqB;oBACvB,CAAC,CAAC,cAAc,CAAC;gBACnB,OAAO,GAAG,CAAC;YACb,CAAC,EAAE,EAAE,CAAC,CAAC;YAEP,MAAM,KAAK,GAAG,MAAM,uBAAA,IAAI,iGAAgC,MAApC,IAAI,EACtB,uBAAuB,CACxB,CAAC;YAEF,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;gBACpB,KAAK,CAAC,aAAa,GAAG;oBACpB,GAAG,KAAK,CAAC,aAAa;oBACtB,GAAG,KAAK;iBACT,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;YACxD,MAAM,KAAK,CAAC;QACd,CAAC;gBAAS,CAAC;YACT,WAAW,EAAE,CAAC;QAChB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACM,OAAO;QACd,KAAK,CAAC,OAAO,EAAE,CAAC;QAChB,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,YAAY,CAAC,EACjB,gBAAgB,GACS;QACzB,MAAM,IAAI,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;IAClD,CAAC;CACF;AAzWD,wDAyWC;;AA/RC;;;;;;GAMG;AACH,KAAK,yDACH,uBAA+C,EAC/C,eAAuB;IAEvB,MAAM,KAAK,GAAuC,EAAE,CAAC;IACrD,IAAI,gBAAgB,GAA2B,EAAE,CAAC;IAElD,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,uBAAA,IAAI,kDAAoB,CAAC,kBAAkB,CAAC;YACjE,YAAY,EAAE,eAAe;YAC7B,cAAc,EAAE,uBAAA,IAAI,8CAAgB;YACpC,gBAAgB,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC,CAAC;SACvE,CAAC,CAAC;QAEH,MAAM,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAC,OAAO,CAC7C,CAAC,CAAC,cAAc,EAAE,eAAe,CAAC,EAAE,EAAE;YACpC,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAC,WAAW,EAAE,CAAC,CAAC;YAErD,IAAI,IAAI,EAAE,KAAK,EAAE,CAAC;gBAChB,KAAK,CAAC,cAAc,CAAC,GAAG;oBACtB,cAAc,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI;oBACjC,cAAc,EAAE,sBAAsB,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;oBACtD,iBAAiB,EAAE,IAAI,EAAE,GAAG;wBAC1B,CAAC,CAAC,sBAAsB,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;wBACtC,CAAC,CAAC,IAAI;iBACT,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,gBAAgB,CAAC,cAAc,CAAC,GAAG,eAAe,CAAC;YACrD,CAAC;QACH,CAAC,CACF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;QACxD,gBAAgB,GAAG,EAAE,GAAG,uBAAuB,EAAE,CAAC;IACpD,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;;;GAQG;AACH,KAAK,mEACH,iBAAyC,EACzC,eAAuB;IAEvB,IAAI,CAAC;QACH,MAAM,KAAK,GAAuC,EAAE,CAAC;QACrD,MAAM,gBAAgB,GAA2B,EAAE,CAAC;QAEpD,MAAM,sBAAsB,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAChD,4BAA4B,CAC7B,CAAC;QACF,MAAM,qBAAqB,GACzB,sBAAsB,CAAC,8BAA8B,CAAC;QAExD,mEAAmE;QACnE,MAAM,kBAAkB,GAAG,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,MAAM,CAEjE,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,eAAe,CAAC,EAAE,EAAE;YAC3C,MAAM,aAAa,GACjB,MAAM,CAAC,OAAO,CAAC,qBAAqB,CACrC,CAAC,IAAI,CACJ,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CACb,MAAM,CAAC,cAAc,CAAC,WAAW,EAAE;gBACnC,eAAe,CAAC,WAAW,EAAE,CAChC,CAAC;YAEF,IAAI,aAAa,EAAE,CAAC;gBAClB,GAAG,CAAC,cAAc,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;YACvE,CAAC;iBAAM,CAAC;gBACN,qDAAqD;gBACrD,gBAAgB,CAAC,cAAc,CAAC,GAAG,eAAe,CAAC;YACrD,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAAE,CAAC,CAAC;QAEP,MAAM,yBAAyB,GAAG,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;QACrE,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC,UAAU,CAC3C,yBAAyB,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,cAAc,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;YACpE,MAAM,kBAAkB,GAAG,IAAA,iCAAqB,EAAC,OAAO,CAAC,CAAC;YAC1D,MAAM,WAAW,GAAG,MAAM,uBAAA,IAAI,kDAAoB,CAAC,gBAAgB,CAAC;gBAClE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,CAAC;gBACvD,QAAQ,EAAE,eAAe;aAC1B,CAAC,CAAC;YAEH,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,CACjC,CAAC,IAAI,EAAE,EAAE,CACP,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE;gBAC/B,kBAAkB,CAAC,WAAW,EAAE,CACnC,CAAC;YAEF,OAAO;gBACL,cAAc;gBACd,cAAc,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI;gBACrD,cAAc,EAAE,UAAU,EAAE,KAAK;oBAC/B,CAAC,CAAC,sBAAsB,CAAC,UAAU,CAAC,KAAK,CAAC;oBAC1C,CAAC,CAAC,IAAI;gBACR,iBAAiB,EAAE,IAAI;aACxB,CAAC;QACJ,CAAC,CAAC,CACH,CAAC;QAEF,YAAY,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;YACrC,MAAM,CAAC,cAAc,EAAE,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,GAClD,yBAAyB,CAAC,KAAK,CAAC,CAAC;YAEnC,IAAI,MAAM,CAAC,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;gBACjE,KAAK,CAAC,cAAc,CAAC,GAAG;oBACtB,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,cAAc;oBAC3C,cAAc,EAAE,MAAM,CAAC,KAAK,CAAC,cAAc;oBAC3C,iBAAiB,EAAE,MAAM,CAAC,KAAK,CAAC,iBAAiB;iBAClD,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;oBACjC,OAAO,CAAC,KAAK,CACX,mCAAmC,cAAc,aAAa,OAAO,EAAE,EACvE,MAAM,CAAC,MAAM,CACd,CAAC;gBACJ,CAAC;gBACD,gBAAgB,CAAC,cAAc,CAAC,GAAG,eAAe,CAAC;YACrD,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IACrC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CACX,2DAA2D,EAC3D,KAAK,CACN,CAAC;QACF,kCAAkC;QAClC,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,gBAAgB,EAAE,EAAE,GAAG,iBAAiB,EAAE,EAAE,CAAC;IACnE,CAAC;AACH,CAAC,uHASC,UAAoB;IAEpB,OAAO,UAAU,CAAC,MAAM,CACtB,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE;QACtB,GAAG,CAAC,cAAc,CAAC,GAAG;YACpB,cAAc,EAAE,IAAI;YACpB,cAAc,EAAE,IAAI;YACpB,iBAAiB,EAAE,IAAI;SACxB,CAAC;QACF,OAAO,GAAG,CAAC;IACb,CAAC,EACD,EAAE,CACH,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,KAAK,iEACH,uBAA+C;IAE/C,MAAM,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;IAEvC,iDAAiD;IACjD,MAAM,EACJ,KAAK,EAAE,aAAa,EACpB,gBAAgB,EAAE,4BAA4B,GAC/C,GAAG,MAAM,uBAAA,IAAI,yFAAwB,MAA5B,IAAI,EACZ,uBAAuB,EACvB,eAAe,CAChB,CAAC;IAEF,oDAAoD;IACpD,IAAI,MAAM,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,oEAAoE;IACpE,MAAM,EACJ,KAAK,EAAE,iBAAiB,EACxB,gBAAgB,EAAE,4BAA4B,GAC/C,GAAG,MAAM,uBAAA,IAAI,mGAAkC,MAAtC,IAAI,EACZ,4BAA4B,EAC5B,eAAe,CAChB,CAAC;IAEF,uEAAuE;IACvE,MAAM,SAAS,GAAG,uBAAA,IAAI,+FAA8B,MAAlC,IAAI,EACpB,MAAM,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAC1C,CAAC;IAEF,4FAA4F;IAC5F,OAAO;QACL,GAAG,SAAS;QACZ,GAAG,iBAAiB;QACpB,GAAG,aAAa;KACjB,CAAC;AACJ,CAAC;AAyEH,kBAAe,sBAAsB,CAAC","sourcesContent":["import type {\n  ControllerGetStateAction,\n  ControllerStateChangeEvent,\n  StateMetadata,\n} from '@metamask/base-controller';\nimport {\n  TESTNET_TICKER_SYMBOLS,\n  FALL_BACK_VS_CURRENCY,\n} from '@metamask/controller-utils';\nimport type { Messenger } from '@metamask/messenger';\nimport type {\n  NetworkControllerGetNetworkClientByIdAction,\n  NetworkControllerGetStateAction,\n  NetworkConfiguration,\n} from '@metamask/network-controller';\nimport { StaticIntervalPollingController } from '@metamask/polling-controller';\nimport type { Hex } from '@metamask/utils';\nimport { Mutex } from 'async-mutex';\n\nimport type { AbstractTokenPricesService } from './token-prices-service/abstract-token-prices-service';\nimport { getNativeTokenAddress } from './token-prices-service/codefi-v2';\n\n/**\n * currencyRates - Object keyed by native currency\n *\n * currencyRates.conversionDate - Timestamp of conversion rate expressed in ms since UNIX epoch\n *\n * currencyRates.conversionRate - Conversion rate from current base asset to the current currency\n *\n * currentCurrency - Currently-active ISO 4217 currency code\n *\n * usdConversionRate - Conversion rate from usd to the current currency\n */\nexport type CurrencyRateState = {\n  currentCurrency: string;\n  currencyRates: Record<\n    string,\n    {\n      conversionDate: number | null;\n      conversionRate: number | null;\n      usdConversionRate: number | null;\n    }\n  >;\n};\n\nconst name = 'CurrencyRateController';\n\nexport type CurrencyRateStateChange = ControllerStateChangeEvent<\n  typeof name,\n  CurrencyRateState\n>;\n\nexport type CurrencyRateControllerEvents = CurrencyRateStateChange;\n\nexport type GetCurrencyRateState = ControllerGetStateAction<\n  typeof name,\n  CurrencyRateState\n>;\n\nexport type CurrencyRateControllerActions = GetCurrencyRateState;\n\ntype AllowedActions =\n  | NetworkControllerGetNetworkClientByIdAction\n  | NetworkControllerGetStateAction;\n\nexport type CurrencyRateMessenger = Messenger<\n  typeof name,\n  CurrencyRateControllerActions | AllowedActions,\n  CurrencyRateControllerEvents\n>;\n\nconst metadata: StateMetadata<CurrencyRateState> = {\n  currentCurrency: {\n    includeInStateLogs: true,\n    persist: true,\n    includeInDebugSnapshot: true,\n    usedInUi: true,\n  },\n  currencyRates: {\n    includeInStateLogs: true,\n    persist: true,\n    includeInDebugSnapshot: true,\n    usedInUi: true,\n  },\n};\n\nconst defaultState = {\n  currentCurrency: 'usd',\n  currencyRates: {\n    ETH: {\n      conversionDate: 0,\n      conversionRate: 0,\n      usdConversionRate: null,\n    },\n  },\n};\n\n/** The input to start polling for the {@link CurrencyRateController} */\ntype CurrencyRatePollingInput = {\n  nativeCurrencies: string[];\n};\n\nconst boundedPrecisionNumber = (value: number, precision = 9): number =>\n  Number(value.toFixed(precision));\n\n/**\n * Controller that passively polls on a set interval for an exchange rate from the current network\n * asset to the user's preferred currency.\n */\n/** Result from attempting to fetch rates from an API */\ntype FetchRatesResult = {\n  /** Successfully fetched rates */\n  rates: CurrencyRateState['currencyRates'];\n  /** Currencies that failed and need fallback or null state */\n  failedCurrencies: Record<string, string>;\n};\n\n/**\n * Controller that passively polls on a set interval for an exchange rate from the current network\n * asset to the user's preferred currency.\n */\nexport class CurrencyRateController extends StaticIntervalPollingController<CurrencyRatePollingInput>()<\n  typeof name,\n  CurrencyRateState,\n  CurrencyRateMessenger\n> {\n  readonly #mutex = new Mutex();\n\n  readonly #includeUsdRate: boolean;\n\n  readonly #useExternalServices: () => boolean;\n\n  readonly #tokenPricesService: AbstractTokenPricesService;\n\n  /**\n   * Creates a CurrencyRateController instance.\n   *\n   * @param options - Constructor options.\n   * @param options.includeUsdRate - Keep track of the USD rate in addition to the current currency rate.\n   * @param options.interval - The polling interval, in milliseconds.\n   * @param options.messenger - A reference to the messenger.\n   * @param options.state - Initial state to set on this controller.\n   * @param options.useExternalServices - Feature Switch for using external services (default: true)\n   * @param options.tokenPricesService - An object in charge of retrieving token prices\n   */\n  constructor({\n    includeUsdRate = false,\n    interval = 180000,\n    useExternalServices = () => true,\n    messenger,\n    state,\n    tokenPricesService,\n  }: {\n    includeUsdRate?: boolean;\n    interval?: number;\n    messenger: CurrencyRateMessenger;\n    state?: Partial<CurrencyRateState>;\n    useExternalServices?: () => boolean;\n    tokenPricesService: AbstractTokenPricesService;\n  }) {\n    super({\n      name,\n      metadata,\n      messenger,\n      state: { ...defaultState, ...state },\n    });\n    this.#includeUsdRate = includeUsdRate;\n    this.#useExternalServices = useExternalServices;\n    this.setIntervalLength(interval);\n    this.#tokenPricesService = tokenPricesService;\n  }\n\n  /**\n   * Sets a currency to track.\n   *\n   * @param currentCurrency - ISO 4217 currency code.\n   */\n  async setCurrentCurrency(currentCurrency: string): Promise<void> {\n    const releaseLock = await this.#mutex.acquire();\n    const nativeCurrencies = Object.keys(this.state.currencyRates);\n    try {\n      this.update(() => {\n        return {\n          ...defaultState,\n          currentCurrency,\n        };\n      });\n    } finally {\n      releaseLock();\n    }\n    // TODO: Either fix this lint violation or explain why it's necessary to ignore.\n    // eslint-disable-next-line @typescript-eslint/no-floating-promises\n    this.updateExchangeRate(nativeCurrencies);\n  }\n\n  /**\n   * Attempts to fetch exchange rates from the primary Price API.\n   *\n   * @param nativeCurrenciesToFetch - Map of native currency to the currency symbol to fetch.\n   * @param currentCurrency - The current fiat currency to get rates for.\n   * @returns Object containing successful rates and currencies that failed.\n   */\n  async #fetchRatesFromPriceApi(\n    nativeCurrenciesToFetch: Record<string, string>,\n    currentCurrency: string,\n  ): Promise<FetchRatesResult> {\n    const rates: CurrencyRateState['currencyRates'] = {};\n    let failedCurrencies: Record<string, string> = {};\n\n    try {\n      const response = await this.#tokenPricesService.fetchExchangeRates({\n        baseCurrency: currentCurrency,\n        includeUsdRate: this.#includeUsdRate,\n        cryptocurrencies: [...new Set(Object.values(nativeCurrenciesToFetch))],\n      });\n\n      Object.entries(nativeCurrenciesToFetch).forEach(\n        ([nativeCurrency, fetchedCurrency]) => {\n          const rate = response[fetchedCurrency.toLowerCase()];\n\n          if (rate?.value) {\n            rates[nativeCurrency] = {\n              conversionDate: Date.now() / 1000,\n              conversionRate: boundedPrecisionNumber(1 / rate.value),\n              usdConversionRate: rate?.usd\n                ? boundedPrecisionNumber(1 / rate.usd)\n                : null,\n            };\n          } else {\n            failedCurrencies[nativeCurrency] = fetchedCurrency;\n          }\n        },\n      );\n    } catch (error) {\n      console.error('Failed to fetch exchange rates.', error);\n      failedCurrencies = { ...nativeCurrenciesToFetch };\n    }\n\n    return { rates, failedCurrencies };\n  }\n\n  /**\n   * Fetches exchange rates from the token prices service as a fallback.\n   * This method is designed to never throw - all errors are handled internally\n   * and result in currencies being marked as failed.\n   *\n   * @param currenciesToFetch - Map of native currencies that need fallback fetching.\n   * @param currentCurrency - The current fiat currency to get rates for.\n   * @returns Object containing successful rates and currencies that failed.\n   */\n  async #fetchRatesFromTokenPricesService(\n    currenciesToFetch: Record<string, string>,\n    currentCurrency: string,\n  ): Promise<FetchRatesResult> {\n    try {\n      const rates: CurrencyRateState['currencyRates'] = {};\n      const failedCurrencies: Record<string, string> = {};\n\n      const networkControllerState = this.messenger.call(\n        'NetworkController:getState',\n      );\n      const networkConfigurations =\n        networkControllerState.networkConfigurationsByChainId;\n\n      // Build a map of nativeCurrency -> chainId for currencies to fetch\n      const currencyToChainIds = Object.entries(currenciesToFetch).reduce<\n        Record<string, { fetchedCurrency: string; chainId: Hex }>\n      >((acc, [nativeCurrency, fetchedCurrency]) => {\n        const matchingEntry = (\n          Object.entries(networkConfigurations) as [Hex, NetworkConfiguration][]\n        ).find(\n          ([, config]) =>\n            config.nativeCurrency.toUpperCase() ===\n            fetchedCurrency.toUpperCase(),\n        );\n\n        if (matchingEntry) {\n          acc[nativeCurrency] = { fetchedCurrency, chainId: matchingEntry[0] };\n        } else {\n          // No matching network configuration - mark as failed\n          failedCurrencies[nativeCurrency] = fetchedCurrency;\n        }\n        return acc;\n      }, {});\n\n      const currencyToChainIdsEntries = Object.entries(currencyToChainIds);\n      const ratesResults = await Promise.allSettled(\n        currencyToChainIdsEntries.map(async ([nativeCurrency, { chainId }]) => {\n          const nativeTokenAddress = getNativeTokenAddress(chainId);\n          const tokenPrices = await this.#tokenPricesService.fetchTokenPrices({\n            assets: [{ chainId, tokenAddress: nativeTokenAddress }],\n            currency: currentCurrency,\n          });\n\n          const tokenPrice = tokenPrices.find(\n            (item) =>\n              item.tokenAddress.toLowerCase() ===\n              nativeTokenAddress.toLowerCase(),\n          );\n\n          return {\n            nativeCurrency,\n            conversionDate: tokenPrice ? Date.now() / 1000 : null,\n            conversionRate: tokenPrice?.price\n              ? boundedPrecisionNumber(tokenPrice.price)\n              : null,\n            usdConversionRate: null,\n          };\n        }),\n      );\n\n      ratesResults.forEach((result, index) => {\n        const [nativeCurrency, { fetchedCurrency, chainId }] =\n          currencyToChainIdsEntries[index];\n\n        if (result.status === 'fulfilled' && result.value.conversionRate) {\n          rates[nativeCurrency] = {\n            conversionDate: result.value.conversionDate,\n            conversionRate: result.value.conversionRate,\n            usdConversionRate: result.value.usdConversionRate,\n          };\n        } else {\n          if (result.status === 'rejected') {\n            console.error(\n              `Failed to fetch token price for ${nativeCurrency} on chain ${chainId}`,\n              result.reason,\n            );\n          }\n          failedCurrencies[nativeCurrency] = fetchedCurrency;\n        }\n      });\n\n      return { rates, failedCurrencies };\n    } catch (error) {\n      console.error(\n        'Failed to fetch exchange rates from token prices service.',\n        error,\n      );\n      // Return all currencies as failed\n      return { rates: {}, failedCurrencies: { ...currenciesToFetch } };\n    }\n  }\n\n  /**\n   * Creates null rate entries for currencies that couldn't be fetched.\n   *\n   * @param currencies - Array of currency symbols to create null entries for.\n   * @returns Null rate entries for all provided currencies.\n   */\n  #createNullRatesForCurrencies(\n    currencies: string[],\n  ): CurrencyRateState['currencyRates'] {\n    return currencies.reduce<CurrencyRateState['currencyRates']>(\n      (acc, nativeCurrency) => {\n        acc[nativeCurrency] = {\n          conversionDate: null,\n          conversionRate: null,\n          usdConversionRate: null,\n        };\n        return acc;\n      },\n      {},\n    );\n  }\n\n  /**\n   * Fetches exchange rates with fallback logic.\n   * First tries the Price API, then falls back to token prices service for any failed currencies.\n   *\n   * @param nativeCurrenciesToFetch - Map of native currency to the currency symbol to fetch.\n   * @returns Exchange rates for all requested currencies.\n   */\n  async #fetchExchangeRatesWithFallback(\n    nativeCurrenciesToFetch: Record<string, string>,\n  ): Promise<CurrencyRateState['currencyRates']> {\n    const { currentCurrency } = this.state;\n\n    // Step 1: Try the Price API exchange rates first\n    const {\n      rates: ratesPriceApi,\n      failedCurrencies: failedCurrenciesFromPriceApi,\n    } = await this.#fetchRatesFromPriceApi(\n      nativeCurrenciesToFetch,\n      currentCurrency,\n    );\n\n    // Step 2: If all currencies succeeded, return early\n    if (Object.keys(failedCurrenciesFromPriceApi).length === 0) {\n      return ratesPriceApi;\n    }\n\n    // Step 3: Fallback using token prices service for failed currencies\n    const {\n      rates: ratesFromFallback,\n      failedCurrencies: failedCurrenciesFromFallback,\n    } = await this.#fetchRatesFromTokenPricesService(\n      failedCurrenciesFromPriceApi,\n      currentCurrency,\n    );\n\n    // Step 4: Create null rates for currencies that failed both approaches\n    const nullRates = this.#createNullRatesForCurrencies(\n      Object.keys(failedCurrenciesFromFallback),\n    );\n\n    // Step 5: Merge all results - Price API rates take priority, then fallback, then null rates\n    return {\n      ...nullRates,\n      ...ratesFromFallback,\n      ...ratesPriceApi,\n    };\n  }\n\n  /**\n   * Updates the exchange rate for the current currency and native currency pairs.\n   *\n   * @param nativeCurrencies - The native currency symbols to fetch exchange rates for.\n   */\n  async updateExchangeRate(\n    nativeCurrencies: (string | undefined)[],\n  ): Promise<void> {\n    if (!this.#useExternalServices()) {\n      return;\n    }\n\n    const releaseLock = await this.#mutex.acquire();\n    try {\n      // For preloaded testnets (Goerli, Sepolia) we want to fetch exchange rate for real ETH.\n      // Map each native currency to the symbol we want to fetch for it.\n      const testnetSymbols = Object.values(TESTNET_TICKER_SYMBOLS);\n      const nativeCurrenciesToFetch = nativeCurrencies.reduce<\n        Record<string, string>\n      >((acc, nativeCurrency) => {\n        if (!nativeCurrency) {\n          return acc;\n        }\n\n        acc[nativeCurrency] = testnetSymbols.includes(nativeCurrency)\n          ? FALL_BACK_VS_CURRENCY\n          : nativeCurrency;\n        return acc;\n      }, {});\n\n      const rates = await this.#fetchExchangeRatesWithFallback(\n        nativeCurrenciesToFetch,\n      );\n\n      this.update((state) => {\n        state.currencyRates = {\n          ...state.currencyRates,\n          ...rates,\n        };\n      });\n    } catch (error) {\n      console.error('Failed to fetch exchange rates.', error);\n      throw error;\n    } finally {\n      releaseLock();\n    }\n  }\n\n  /**\n   * Prepare to discard this controller.\n   *\n   * This stops any active polling.\n   */\n  override destroy(): void {\n    super.destroy();\n    this.stopAllPolling();\n  }\n\n  /**\n   * Updates exchange rate for the current currency.\n   *\n   * @param input - The input for the poll.\n   * @param input.nativeCurrencies - The native currency symbols to poll prices for.\n   */\n  async _executePoll({\n    nativeCurrencies,\n  }: CurrencyRatePollingInput): Promise<void> {\n    await this.updateExchangeRate(nativeCurrencies);\n  }\n}\n\nexport default CurrencyRateController;\n"]}