@metamask-previews/assets-controllers 111.1.1-preview-580e1b9da → 111.1.1-preview-429fcf584
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +0 -2
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.cjs.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.d.cts +13 -13
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.d.mts +13 -13
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.mjs.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.cjs +25 -26
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.cjs.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.d.cts +21 -22
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.d.cts.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.d.mts +21 -22
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.d.mts.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.mjs +25 -26
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.mjs.map +1 -1
- package/dist/DeFiPositionsController/group-defi-positions-v6.cjs +52 -56
- package/dist/DeFiPositionsController/group-defi-positions-v6.cjs.map +1 -1
- package/dist/DeFiPositionsController/group-defi-positions-v6.d.cts +2 -3
- package/dist/DeFiPositionsController/group-defi-positions-v6.d.cts.map +1 -1
- package/dist/DeFiPositionsController/group-defi-positions-v6.d.mts +2 -3
- package/dist/DeFiPositionsController/group-defi-positions-v6.d.mts.map +1 -1
- package/dist/DeFiPositionsController/group-defi-positions-v6.mjs +52 -56
- package/dist/DeFiPositionsController/group-defi-positions-v6.mjs.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,9 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Changed
|
|
11
11
|
|
|
12
|
-
- Update `DeFiPositionsControllerV2` for the flat Accounts API v6 balances response: processing polling is driven by the response-level `processingDefiPositions` account-ID array ([#9911](https://github.com/MetaMask/core/pull/9911))
|
|
13
12
|
- Bump `@metamask/network-enablement-controller` from `^6.0.3` to `^6.0.4` ([#9923](https://github.com/MetaMask/core/pull/9923))
|
|
14
|
-
- Bump `@metamask/remote-feature-flag-controller` from `^5.0.0` to `^6.0.0` ([#9945](https://github.com/MetaMask/core/pull/9945))
|
|
15
13
|
|
|
16
14
|
## [111.1.1]
|
|
17
15
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeFiPositionsControllerV2-method-action-types.cjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { DeFiPositionsControllerV2 } from './DeFiPositionsControllerV2.js';\n\n/**\n * Fetches DeFi positions for the selected account group. State is updated\n *
|
|
1
|
+
{"version":3,"file":"DeFiPositionsControllerV2-method-action-types.cjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { DeFiPositionsControllerV2 } from './DeFiPositionsControllerV2.js';\n\n/**\n * Fetches DeFi positions for the selected account group. State is updated only\n * when every selected account in the response is ready (none report\n * `processingDefiPositions`); each account key in that response replaces that\n * account's state (other accounts stay). While any account is still indexing,\n * prior state is kept and the method polls (invalidating the balances cache\n * between attempts) until all selected accounts are ready, the attempt limit\n * is reached, or a request fails. Concurrent calls for the same selected\n * accounts and `vsCurrency` share one in-flight promise; calls for a different\n * selection or fiat currency start a new fetch and leave prior polls running\n * so a later switch back can join them. When a successful ready response\n * required more than one attempt, or when polling hits the attempt limit\n * while still processing, reports to Sentry via `messenger.captureException`\n * (error names `DeFiPositionsV2FetchAttempts` /\n * `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.\n * No-ops when disabled or when the group has no\n * supported accounts. Caching / spam prevention is handled by the apiClient\n * TanStack Query cache (keyed by accounts + query options including\n * `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the\n * first attempt (e.g. pull-to-refresh).\n *\n * @param options - Optional fetch modifiers.\n * @param options.forceRefresh - When true, bypass the apiClient cache on the\n * first attempt and fetch immediately.\n * @returns Resolves when the fetch (and any processing polls) finish.\n */\nexport type DeFiPositionsControllerV2FetchDeFiPositionsAction = {\n type: `DeFiPositionsControllerV2:fetchDeFiPositions`;\n handler: DeFiPositionsControllerV2['fetchDeFiPositions'];\n};\n\n/**\n * Union of all DeFiPositionsControllerV2 action types.\n */\nexport type DeFiPositionsControllerV2MethodActions =\n DeFiPositionsControllerV2FetchDeFiPositionsAction;\n"]}
|
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { DeFiPositionsControllerV2 } from "./DeFiPositionsControllerV2.cjs";
|
|
6
6
|
/**
|
|
7
|
-
* Fetches DeFi positions for the selected account group. State is updated
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* stay). While any account is still indexing,
|
|
11
|
-
* method polls (invalidating the balances cache
|
|
12
|
-
* accounts are
|
|
13
|
-
* Concurrent calls for the same selected
|
|
14
|
-
* in-flight promise; calls for a different
|
|
15
|
-
* a new fetch and leave prior polls running
|
|
16
|
-
* them. When a successful ready response
|
|
17
|
-
* when polling hits the attempt limit
|
|
18
|
-
* Sentry via `messenger.captureException`
|
|
19
|
-
* `DeFiPositionsV2FetchAttempts` /
|
|
7
|
+
* Fetches DeFi positions for the selected account group. State is updated only
|
|
8
|
+
* when every selected account in the response is ready (none report
|
|
9
|
+
* `processingDefiPositions`); each account key in that response replaces that
|
|
10
|
+
* account's state (other accounts stay). While any account is still indexing,
|
|
11
|
+
* prior state is kept and the method polls (invalidating the balances cache
|
|
12
|
+
* between attempts) until all selected accounts are ready, the attempt limit
|
|
13
|
+
* is reached, or a request fails. Concurrent calls for the same selected
|
|
14
|
+
* accounts and `vsCurrency` share one in-flight promise; calls for a different
|
|
15
|
+
* selection or fiat currency start a new fetch and leave prior polls running
|
|
16
|
+
* so a later switch back can join them. When a successful ready response
|
|
17
|
+
* required more than one attempt, or when polling hits the attempt limit
|
|
18
|
+
* while still processing, reports to Sentry via `messenger.captureException`
|
|
19
|
+
* (error names `DeFiPositionsV2FetchAttempts` /
|
|
20
20
|
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
21
21
|
* No-ops when disabled or when the group has no
|
|
22
22
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { DeFiPositionsControllerV2 } from "./DeFiPositionsControllerV2.mjs";
|
|
6
6
|
/**
|
|
7
|
-
* Fetches DeFi positions for the selected account group. State is updated
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* stay). While any account is still indexing,
|
|
11
|
-
* method polls (invalidating the balances cache
|
|
12
|
-
* accounts are
|
|
13
|
-
* Concurrent calls for the same selected
|
|
14
|
-
* in-flight promise; calls for a different
|
|
15
|
-
* a new fetch and leave prior polls running
|
|
16
|
-
* them. When a successful ready response
|
|
17
|
-
* when polling hits the attempt limit
|
|
18
|
-
* Sentry via `messenger.captureException`
|
|
19
|
-
* `DeFiPositionsV2FetchAttempts` /
|
|
7
|
+
* Fetches DeFi positions for the selected account group. State is updated only
|
|
8
|
+
* when every selected account in the response is ready (none report
|
|
9
|
+
* `processingDefiPositions`); each account key in that response replaces that
|
|
10
|
+
* account's state (other accounts stay). While any account is still indexing,
|
|
11
|
+
* prior state is kept and the method polls (invalidating the balances cache
|
|
12
|
+
* between attempts) until all selected accounts are ready, the attempt limit
|
|
13
|
+
* is reached, or a request fails. Concurrent calls for the same selected
|
|
14
|
+
* accounts and `vsCurrency` share one in-flight promise; calls for a different
|
|
15
|
+
* selection or fiat currency start a new fetch and leave prior polls running
|
|
16
|
+
* so a later switch back can join them. When a successful ready response
|
|
17
|
+
* required more than one attempt, or when polling hits the attempt limit
|
|
18
|
+
* while still processing, reports to Sentry via `messenger.captureException`
|
|
19
|
+
* (error names `DeFiPositionsV2FetchAttempts` /
|
|
20
20
|
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
21
21
|
* No-ops when disabled or when the group has no
|
|
22
22
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeFiPositionsControllerV2-method-action-types.mjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { DeFiPositionsControllerV2 } from './DeFiPositionsControllerV2.js';\n\n/**\n * Fetches DeFi positions for the selected account group. State is updated\n *
|
|
1
|
+
{"version":3,"file":"DeFiPositionsControllerV2-method-action-types.mjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG","sourcesContent":["/**\n * This file is auto generated.\n * Do not edit manually.\n */\n\nimport type { DeFiPositionsControllerV2 } from './DeFiPositionsControllerV2.js';\n\n/**\n * Fetches DeFi positions for the selected account group. State is updated only\n * when every selected account in the response is ready (none report\n * `processingDefiPositions`); each account key in that response replaces that\n * account's state (other accounts stay). While any account is still indexing,\n * prior state is kept and the method polls (invalidating the balances cache\n * between attempts) until all selected accounts are ready, the attempt limit\n * is reached, or a request fails. Concurrent calls for the same selected\n * accounts and `vsCurrency` share one in-flight promise; calls for a different\n * selection or fiat currency start a new fetch and leave prior polls running\n * so a later switch back can join them. When a successful ready response\n * required more than one attempt, or when polling hits the attempt limit\n * while still processing, reports to Sentry via `messenger.captureException`\n * (error names `DeFiPositionsV2FetchAttempts` /\n * `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.\n * No-ops when disabled or when the group has no\n * supported accounts. Caching / spam prevention is handled by the apiClient\n * TanStack Query cache (keyed by accounts + query options including\n * `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the\n * first attempt (e.g. pull-to-refresh).\n *\n * @param options - Optional fetch modifiers.\n * @param options.forceRefresh - When true, bypass the apiClient cache on the\n * first attempt and fetch immediately.\n * @returns Resolves when the fetch (and any processing polls) finish.\n */\nexport type DeFiPositionsControllerV2FetchDeFiPositionsAction = {\n type: `DeFiPositionsControllerV2:fetchDeFiPositions`;\n handler: DeFiPositionsControllerV2['fetchDeFiPositions'];\n};\n\n/**\n * Union of all DeFiPositionsControllerV2 action types.\n */\nexport type DeFiPositionsControllerV2MethodActions =\n DeFiPositionsControllerV2FetchDeFiPositionsAction;\n"]}
|
|
@@ -50,15 +50,14 @@ exports.getDefaultDeFiPositionsControllerV2State = getDefaultDeFiPositionsContro
|
|
|
50
50
|
* `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.
|
|
51
51
|
* pull-to-refresh); later processing polls always bypass the cache.
|
|
52
52
|
*
|
|
53
|
-
* When the API reports
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* in-flight fetch for that group and currency.
|
|
53
|
+
* When the API reports `processingDefiPositions` for any account, this
|
|
54
|
+
* controller polls until indexing finishes or the attempt limit is reached,
|
|
55
|
+
* and only then processes and writes state — mixed ready/processing responses
|
|
56
|
+
* leave prior state untouched. Concurrent calls for the same selected accounts
|
|
57
|
+
* and `vsCurrency` share one in-flight promise so the UI can treat the pending
|
|
58
|
+
* promise as a loading signal. Calls for a different selection or fiat currency
|
|
59
|
+
* start their own fetch and leave any prior poll running, so switching back can
|
|
60
|
+
* join an in-flight fetch for that group and currency.
|
|
62
61
|
*
|
|
63
62
|
* Processing-poll `maxAttempts` / `pollInterval` are read via
|
|
64
63
|
* {@link getProcessingPollConfig} from the `defiControllerV2` remote feature
|
|
@@ -102,19 +101,19 @@ class DeFiPositionsControllerV2 extends base_controller_1.BaseController {
|
|
|
102
101
|
this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
|
|
103
102
|
}
|
|
104
103
|
/**
|
|
105
|
-
* Fetches DeFi positions for the selected account group. State is updated
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* stay). While any account is still indexing,
|
|
109
|
-
* method polls (invalidating the balances cache
|
|
110
|
-
* accounts are
|
|
111
|
-
* Concurrent calls for the same selected
|
|
112
|
-
* in-flight promise; calls for a different
|
|
113
|
-
* a new fetch and leave prior polls running
|
|
114
|
-
* them. When a successful ready response
|
|
115
|
-
* when polling hits the attempt limit
|
|
116
|
-
* Sentry via `messenger.captureException`
|
|
117
|
-
* `DeFiPositionsV2FetchAttempts` /
|
|
104
|
+
* Fetches DeFi positions for the selected account group. State is updated only
|
|
105
|
+
* when every selected account in the response is ready (none report
|
|
106
|
+
* `processingDefiPositions`); each account key in that response replaces that
|
|
107
|
+
* account's state (other accounts stay). While any account is still indexing,
|
|
108
|
+
* prior state is kept and the method polls (invalidating the balances cache
|
|
109
|
+
* between attempts) until all selected accounts are ready, the attempt limit
|
|
110
|
+
* is reached, or a request fails. Concurrent calls for the same selected
|
|
111
|
+
* accounts and `vsCurrency` share one in-flight promise; calls for a different
|
|
112
|
+
* selection or fiat currency start a new fetch and leave prior polls running
|
|
113
|
+
* so a later switch back can join them. When a successful ready response
|
|
114
|
+
* required more than one attempt, or when polling hits the attempt limit
|
|
115
|
+
* while still processing, reports to Sentry via `messenger.captureException`
|
|
116
|
+
* (error names `DeFiPositionsV2FetchAttempts` /
|
|
118
117
|
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
119
118
|
* No-ops when disabled or when the group has no
|
|
120
119
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
@@ -185,10 +184,10 @@ _DeFiPositionsControllerV2_apiClient = new WeakMap(), _DeFiPositionsControllerV2
|
|
|
185
184
|
console.error('Failed to fetch DeFi positions', error);
|
|
186
185
|
return;
|
|
187
186
|
}
|
|
188
|
-
const stillProcessing =
|
|
189
|
-
// Only process and write when
|
|
190
|
-
//
|
|
191
|
-
//
|
|
187
|
+
const stillProcessing = response.accounts.some((account) => account.processingDefiPositions);
|
|
188
|
+
// Only process and write when every account is ready so a partial
|
|
189
|
+
// response cannot clear or overwrite positions for accounts that are
|
|
190
|
+
// still indexing.
|
|
192
191
|
if (!stillProcessing) {
|
|
193
192
|
const positionsByAccount = (0, group_defi_positions_v6_js_1.groupDeFiPositionsV6)(response, internalAccountIdByCaip);
|
|
194
193
|
this.update((state) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeFiPositionsControllerV2.cjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccountTreeControllerGetAccountsFromSelectedAccountGroupAction } from '@metamask/account-tree-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n ControllerGetStateAction,\n ControllerStateChangedEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport type { ApiPlatformClient } from '@metamask/core-backend';\nimport type { Messenger } from '@metamask/messenger';\nimport type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';\n\nimport { buildDeFiBalancesQuery } from './build-defi-balances-query.js';\nimport type { DeFiBalancesQuery } from './build-defi-balances-query.js';\nimport { getProcessingPollConfig } from './defi-controller-v2-feature-flag.js';\nimport type { DeFiPositionsControllerV2MethodActions } from './DeFiPositionsControllerV2-method-action-types.js';\nimport type { DeFiPositionsByAccount } from './group-defi-positions-v6.js';\nimport { groupDeFiPositionsV6 } from './group-defi-positions-v6.js';\n\nconst controllerName = 'DeFiPositionsControllerV2';\n\nconst MESSENGER_EXPOSED_METHODS = ['fetchDeFiPositions'] as const;\n\n/**\n * @param ms - Milliseconds to wait.\n * @returns A promise that resolves after `ms`.\n */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport type DeFiPositionsControllerV2State = {\n /**\n * DeFi positions keyed by internal MetaMask account ID (`InternalAccount.id`,\n * the same key AssetsController uses). Each account maps to a flat list of\n * protocol groups shown in the DeFi tab, each carrying its own `chainId` for\n * filtering plus the details-page sections embedded inside it. This is\n * exactly the shape the client consumes, so no further transformation is\n * needed on read.\n *\n * Named `allDeFiPositionsV2` (rather than `allDeFiPositions`) so it can live\n * alongside the legacy `DeFiPositionsController` in clients that flatten every\n * controller's state into a single object (e.g. the extension background),\n * without colliding on the shared `allDeFiPositions` key.\n */\n allDeFiPositionsV2: DeFiPositionsByAccount;\n};\n\nconst controllerMetadata: StateMetadata<DeFiPositionsControllerV2State> = {\n allDeFiPositionsV2: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n};\n\nexport const getDefaultDeFiPositionsControllerV2State =\n (): DeFiPositionsControllerV2State => {\n return {\n allDeFiPositionsV2: {},\n };\n };\n\nexport type DeFiPositionsControllerV2GetStateAction = ControllerGetStateAction<\n typeof controllerName,\n DeFiPositionsControllerV2State\n>;\n\nexport type DeFiPositionsControllerV2Actions =\n | DeFiPositionsControllerV2GetStateAction\n | DeFiPositionsControllerV2MethodActions;\n\nexport type DeFiPositionsControllerV2StateChangedEvent =\n ControllerStateChangedEvent<\n typeof controllerName,\n DeFiPositionsControllerV2State\n >;\n\nexport type DeFiPositionsControllerV2Events =\n DeFiPositionsControllerV2StateChangedEvent;\n\n/**\n * The external actions available to the {@link DeFiPositionsControllerV2}.\n */\nexport type AllowedActions =\n | AccountTreeControllerGetAccountsFromSelectedAccountGroupAction\n | RemoteFeatureFlagControllerGetStateAction;\n\n/**\n * The external events available to the {@link DeFiPositionsControllerV2}.\n *\n * None yet — clients must call `fetchDeFiPositions` (and optionally\n * `{ forceRefresh: true }`) on their own triggers. Likely future subscriptions:\n * `AccountTreeController:selectedAccountGroupChange`,\n * `TransactionController:transactionConfirmed`, and `KeyringController:lock`.\n */\nexport type AllowedEvents = never;\n\nexport type DeFiPositionsControllerV2Messenger = Messenger<\n typeof controllerName,\n DeFiPositionsControllerV2Actions | AllowedActions,\n DeFiPositionsControllerV2Events | AllowedEvents\n>;\n\n/**\n * Controller that fetches DeFi positions for the selected account group from\n * the Accounts API (v6 multiaccount balances) and stores them in the shape the\n * client consumes directly.\n *\n * Deduplication and freshness are handled by the shared TanStack Query cache on\n * {@link ApiPlatformClient} (balances default `staleTime` is 1 minute). Pass\n * `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.\n * pull-to-refresh); later processing polls always bypass the cache.\n *\n * When the API reports account IDs in the response-level\n * `processingDefiPositions` array, this controller polls until indexing\n * finishes or the attempt limit is reached, and only then processes and writes\n * state — responses that still list processing accounts leave prior state\n * untouched. Concurrent calls for the same selected accounts and `vsCurrency`\n * share one in-flight promise so the UI can treat the pending promise as a\n * loading signal. Calls for a different selection or fiat currency start their\n * own fetch and leave any prior poll running, so switching back can join an\n * in-flight fetch for that group and currency.\n *\n * Processing-poll `maxAttempts` / `pollInterval` are read via\n * {@link getProcessingPollConfig} from the `defiControllerV2` remote feature\n * flag (`RemoteFeatureFlagController:getState`), falling back to built-in\n * defaults when unset or invalid. Clients must delegate that action to this\n * controller's messenger.\n */\nexport class DeFiPositionsControllerV2 extends BaseController<\n typeof controllerName,\n DeFiPositionsControllerV2State,\n DeFiPositionsControllerV2Messenger\n> {\n readonly #apiClient: ApiPlatformClient;\n\n readonly #isEnabled: () => boolean;\n\n readonly #getVsCurrency: () => string;\n\n /**\n * In-flight fetches keyed by selected DeFi-queryable account IDs plus\n * `vsCurrency`. Concurrent callers for the same selection and currency share\n * a promise; different selections or fiat currencies keep independent\n * fetches so fast switching can join an earlier matching poll.\n */\n readonly #inFlightFetches = new Map<string, Promise<void>>();\n\n /**\n * @param options - Constructor options.\n * @param options.messenger - The controller messenger.\n * @param options.apiClient - Accounts API client used to fetch balances/positions. Auth is handled by the client.\n * @param options.isEnabled - Returns whether fetching is enabled (default: () => false).\n * @param options.getVsCurrency - Returns the fiat currency for prices (default: () => 'usd').\n * @param options.state - Initial controller state.\n */\n constructor({\n messenger,\n apiClient,\n isEnabled,\n getVsCurrency,\n state,\n }: {\n messenger: DeFiPositionsControllerV2Messenger;\n apiClient: ApiPlatformClient;\n isEnabled: () => boolean;\n getVsCurrency: () => string;\n state?: Partial<DeFiPositionsControllerV2State>;\n }) {\n super({\n name: controllerName,\n metadata: controllerMetadata,\n messenger,\n state: {\n ...getDefaultDeFiPositionsControllerV2State(),\n ...state,\n },\n });\n\n this.#apiClient = apiClient;\n this.#isEnabled = isEnabled;\n this.#getVsCurrency = getVsCurrency;\n\n this.messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Fetches DeFi positions for the selected account group. State is updated\n * only when the response lists no accounts in its `processingDefiPositions`\n * array; each resolved account then has its state replaced (other accounts\n * stay). While any account is still indexing, prior state is kept and the\n * method polls (invalidating the balances cache between attempts) until no\n * accounts are processing, the attempt limit is reached, or a request fails.\n * Concurrent calls for the same selected accounts and `vsCurrency` share one\n * in-flight promise; calls for a different selection or fiat currency start\n * a new fetch and leave prior polls running so a later switch back can join\n * them. When a successful ready response required more than one attempt, or\n * when polling hits the attempt limit while still processing, reports to\n * Sentry via `messenger.captureException` (error names\n * `DeFiPositionsV2FetchAttempts` /\n * `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.\n * No-ops when disabled or when the group has no\n * supported accounts. Caching / spam prevention is handled by the apiClient\n * TanStack Query cache (keyed by accounts + query options including\n * `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the\n * first attempt (e.g. pull-to-refresh).\n *\n * @param options - Optional fetch modifiers.\n * @param options.forceRefresh - When true, bypass the apiClient cache on the\n * first attempt and fetch immediately.\n * @returns Resolves when the fetch (and any processing polls) finish.\n */\n async fetchDeFiPositions(options?: {\n forceRefresh?: boolean;\n }): Promise<void> {\n if (!this.#isEnabled()) {\n return;\n }\n\n const selectedAccounts = this.messenger.call(\n 'AccountTreeController:getAccountsFromSelectedAccountGroup',\n );\n const query = buildDeFiBalancesQuery(selectedAccounts);\n const vsCurrency = this.#getVsCurrency().toLowerCase();\n // Include vsCurrency so a fiat change does not join a poll priced in the\n // previous currency (TanStack also keys the balances cache on vsCurrency).\n const inFlightKey = [\n ...[...query.internalAccountIdByCaip.keys()].sort(),\n vsCurrency,\n ].join('\\0');\n\n const existing = this.#inFlightFetches.get(inFlightKey);\n if (existing) {\n await existing;\n return;\n }\n\n // Same-key callers join this promise instead of replacing it, so cleanup\n // can always delete without checking identity.\n const fetchPromise = this.#fetchDeFiPositions(\n options,\n query,\n vsCurrency,\n ).finally(() => {\n this.#inFlightFetches.delete(inFlightKey);\n });\n this.#inFlightFetches.set(inFlightKey, fetchPromise);\n\n await fetchPromise;\n }\n\n async #fetchDeFiPositions(\n options: { forceRefresh?: boolean } | undefined,\n { networks, internalAccountIdByCaip }: DeFiBalancesQuery,\n vsCurrency: string,\n ): Promise<void> {\n if (internalAccountIdByCaip.size === 0 || networks.length === 0) {\n return;\n }\n\n const accountIds = [...internalAccountIdByCaip.keys()];\n const queryOptions = {\n networks,\n includeDeFiBalances: true,\n forceFetchDeFiPositions: true,\n includePrices: true,\n vsCurrency,\n };\n\n // Resolve once per fetch so a mid-poll remote-flag change cannot stretch\n // or shrink this poll sequence inconsistently.\n const { maxAttempts, pollInterval } = getProcessingPollConfig(\n this.messenger,\n );\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n // First attempt respects forceRefresh; later polls always bypass cache\n // so we do not spin on a stale processing snapshot.\n const fetchOptions = {\n ...(options?.forceRefresh || attempt > 0 ? { staleTime: 0 } : {}),\n };\n\n let response;\n try {\n response = await this.#apiClient.accounts.fetchV6MultiAccountBalances(\n accountIds,\n queryOptions,\n fetchOptions,\n );\n } catch (error) {\n // Soft-fail so prior state stays and the in-flight promise settles.\n console.error('Failed to fetch DeFi positions', error);\n return;\n }\n\n const stillProcessing =\n (response.processingDefiPositions?.length ?? 0) > 0;\n\n // Only process and write when no account is still indexing, so a\n // partial response (whose DeFi rows for processing accounts are\n // omitted) cannot clear or overwrite positions for those accounts.\n if (!stillProcessing) {\n const positionsByAccount = groupDeFiPositionsV6(\n response,\n internalAccountIdByCaip,\n );\n\n this.update((state) => {\n for (const [accountId, positions] of Object.entries(\n positionsByAccount,\n )) {\n state.allDeFiPositionsV2[accountId] = positions;\n }\n });\n\n // Report how many attempts were needed (only when polling was\n // required) so Sentry can inform remote-flag / default poll-limit\n // tuning without flooding on first-try successes.\n const attemptsTaken = attempt + 1;\n if (attemptsTaken > 1) {\n const multipleAttemptsError = new Error(\n `DeFiPositionsControllerV2: positions ready after ${attemptsTaken} attempt(s)`,\n );\n multipleAttemptsError.name = 'DeFiPositionsV2FetchAttempts';\n this.messenger.captureException?.(multipleAttemptsError);\n }\n return;\n }\n\n const { queryKey } =\n this.#apiClient.accounts.getV6MultiAccountBalancesQueryOptions(\n accountIds,\n queryOptions,\n fetchOptions,\n );\n await this.#apiClient.accounts.queryClient.invalidateQueries({\n queryKey,\n });\n\n const isLastAttempt = attempt >= maxAttempts - 1;\n if (isLastAttempt) {\n // Report exhausted polls so Sentry can inform remote-flag / default\n // poll-limit tuning when indexing never finishes in time.\n const multipleAttemptsError = new Error(\n `DeFiPositionsControllerV2: still processing after ${maxAttempts} attempt(s)`,\n );\n multipleAttemptsError.name = 'DeFiPositionsV2ProcessingPollExhausted';\n this.messenger.captureException?.(multipleAttemptsError);\n return;\n }\n\n await delay(pollInterval);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"DeFiPositionsControllerV2.cjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccountTreeControllerGetAccountsFromSelectedAccountGroupAction } from '@metamask/account-tree-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n ControllerGetStateAction,\n ControllerStateChangedEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport type { ApiPlatformClient } from '@metamask/core-backend';\nimport type { Messenger } from '@metamask/messenger';\nimport type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';\n\nimport { buildDeFiBalancesQuery } from './build-defi-balances-query.js';\nimport type { DeFiBalancesQuery } from './build-defi-balances-query.js';\nimport { getProcessingPollConfig } from './defi-controller-v2-feature-flag.js';\nimport type { DeFiPositionsControllerV2MethodActions } from './DeFiPositionsControllerV2-method-action-types.js';\nimport type { DeFiPositionsByAccount } from './group-defi-positions-v6.js';\nimport { groupDeFiPositionsV6 } from './group-defi-positions-v6.js';\n\nconst controllerName = 'DeFiPositionsControllerV2';\n\nconst MESSENGER_EXPOSED_METHODS = ['fetchDeFiPositions'] as const;\n\n/**\n * @param ms - Milliseconds to wait.\n * @returns A promise that resolves after `ms`.\n */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport type DeFiPositionsControllerV2State = {\n /**\n * DeFi positions keyed by internal MetaMask account ID (`InternalAccount.id`,\n * the same key AssetsController uses). Each account maps to a flat list of\n * protocol groups shown in the DeFi tab, each carrying its own `chainId` for\n * filtering plus the details-page sections embedded inside it. This is\n * exactly the shape the client consumes, so no further transformation is\n * needed on read.\n *\n * Named `allDeFiPositionsV2` (rather than `allDeFiPositions`) so it can live\n * alongside the legacy `DeFiPositionsController` in clients that flatten every\n * controller's state into a single object (e.g. the extension background),\n * without colliding on the shared `allDeFiPositions` key.\n */\n allDeFiPositionsV2: DeFiPositionsByAccount;\n};\n\nconst controllerMetadata: StateMetadata<DeFiPositionsControllerV2State> = {\n allDeFiPositionsV2: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n};\n\nexport const getDefaultDeFiPositionsControllerV2State =\n (): DeFiPositionsControllerV2State => {\n return {\n allDeFiPositionsV2: {},\n };\n };\n\nexport type DeFiPositionsControllerV2GetStateAction = ControllerGetStateAction<\n typeof controllerName,\n DeFiPositionsControllerV2State\n>;\n\nexport type DeFiPositionsControllerV2Actions =\n | DeFiPositionsControllerV2GetStateAction\n | DeFiPositionsControllerV2MethodActions;\n\nexport type DeFiPositionsControllerV2StateChangedEvent =\n ControllerStateChangedEvent<\n typeof controllerName,\n DeFiPositionsControllerV2State\n >;\n\nexport type DeFiPositionsControllerV2Events =\n DeFiPositionsControllerV2StateChangedEvent;\n\n/**\n * The external actions available to the {@link DeFiPositionsControllerV2}.\n */\nexport type AllowedActions =\n | AccountTreeControllerGetAccountsFromSelectedAccountGroupAction\n | RemoteFeatureFlagControllerGetStateAction;\n\n/**\n * The external events available to the {@link DeFiPositionsControllerV2}.\n *\n * None yet — clients must call `fetchDeFiPositions` (and optionally\n * `{ forceRefresh: true }`) on their own triggers. Likely future subscriptions:\n * `AccountTreeController:selectedAccountGroupChange`,\n * `TransactionController:transactionConfirmed`, and `KeyringController:lock`.\n */\nexport type AllowedEvents = never;\n\nexport type DeFiPositionsControllerV2Messenger = Messenger<\n typeof controllerName,\n DeFiPositionsControllerV2Actions | AllowedActions,\n DeFiPositionsControllerV2Events | AllowedEvents\n>;\n\n/**\n * Controller that fetches DeFi positions for the selected account group from\n * the Accounts API (v6 multiaccount balances) and stores them in the shape the\n * client consumes directly.\n *\n * Deduplication and freshness are handled by the shared TanStack Query cache on\n * {@link ApiPlatformClient} (balances default `staleTime` is 1 minute). Pass\n * `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.\n * pull-to-refresh); later processing polls always bypass the cache.\n *\n * When the API reports `processingDefiPositions` for any account, this\n * controller polls until indexing finishes or the attempt limit is reached,\n * and only then processes and writes state — mixed ready/processing responses\n * leave prior state untouched. Concurrent calls for the same selected accounts\n * and `vsCurrency` share one in-flight promise so the UI can treat the pending\n * promise as a loading signal. Calls for a different selection or fiat currency\n * start their own fetch and leave any prior poll running, so switching back can\n * join an in-flight fetch for that group and currency.\n *\n * Processing-poll `maxAttempts` / `pollInterval` are read via\n * {@link getProcessingPollConfig} from the `defiControllerV2` remote feature\n * flag (`RemoteFeatureFlagController:getState`), falling back to built-in\n * defaults when unset or invalid. Clients must delegate that action to this\n * controller's messenger.\n */\nexport class DeFiPositionsControllerV2 extends BaseController<\n typeof controllerName,\n DeFiPositionsControllerV2State,\n DeFiPositionsControllerV2Messenger\n> {\n readonly #apiClient: ApiPlatformClient;\n\n readonly #isEnabled: () => boolean;\n\n readonly #getVsCurrency: () => string;\n\n /**\n * In-flight fetches keyed by selected DeFi-queryable account IDs plus\n * `vsCurrency`. Concurrent callers for the same selection and currency share\n * a promise; different selections or fiat currencies keep independent\n * fetches so fast switching can join an earlier matching poll.\n */\n readonly #inFlightFetches = new Map<string, Promise<void>>();\n\n /**\n * @param options - Constructor options.\n * @param options.messenger - The controller messenger.\n * @param options.apiClient - Accounts API client used to fetch balances/positions. Auth is handled by the client.\n * @param options.isEnabled - Returns whether fetching is enabled (default: () => false).\n * @param options.getVsCurrency - Returns the fiat currency for prices (default: () => 'usd').\n * @param options.state - Initial controller state.\n */\n constructor({\n messenger,\n apiClient,\n isEnabled,\n getVsCurrency,\n state,\n }: {\n messenger: DeFiPositionsControllerV2Messenger;\n apiClient: ApiPlatformClient;\n isEnabled: () => boolean;\n getVsCurrency: () => string;\n state?: Partial<DeFiPositionsControllerV2State>;\n }) {\n super({\n name: controllerName,\n metadata: controllerMetadata,\n messenger,\n state: {\n ...getDefaultDeFiPositionsControllerV2State(),\n ...state,\n },\n });\n\n this.#apiClient = apiClient;\n this.#isEnabled = isEnabled;\n this.#getVsCurrency = getVsCurrency;\n\n this.messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Fetches DeFi positions for the selected account group. State is updated only\n * when every selected account in the response is ready (none report\n * `processingDefiPositions`); each account key in that response replaces that\n * account's state (other accounts stay). While any account is still indexing,\n * prior state is kept and the method polls (invalidating the balances cache\n * between attempts) until all selected accounts are ready, the attempt limit\n * is reached, or a request fails. Concurrent calls for the same selected\n * accounts and `vsCurrency` share one in-flight promise; calls for a different\n * selection or fiat currency start a new fetch and leave prior polls running\n * so a later switch back can join them. When a successful ready response\n * required more than one attempt, or when polling hits the attempt limit\n * while still processing, reports to Sentry via `messenger.captureException`\n * (error names `DeFiPositionsV2FetchAttempts` /\n * `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.\n * No-ops when disabled or when the group has no\n * supported accounts. Caching / spam prevention is handled by the apiClient\n * TanStack Query cache (keyed by accounts + query options including\n * `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the\n * first attempt (e.g. pull-to-refresh).\n *\n * @param options - Optional fetch modifiers.\n * @param options.forceRefresh - When true, bypass the apiClient cache on the\n * first attempt and fetch immediately.\n * @returns Resolves when the fetch (and any processing polls) finish.\n */\n async fetchDeFiPositions(options?: {\n forceRefresh?: boolean;\n }): Promise<void> {\n if (!this.#isEnabled()) {\n return;\n }\n\n const selectedAccounts = this.messenger.call(\n 'AccountTreeController:getAccountsFromSelectedAccountGroup',\n );\n const query = buildDeFiBalancesQuery(selectedAccounts);\n const vsCurrency = this.#getVsCurrency().toLowerCase();\n // Include vsCurrency so a fiat change does not join a poll priced in the\n // previous currency (TanStack also keys the balances cache on vsCurrency).\n const inFlightKey = [\n ...[...query.internalAccountIdByCaip.keys()].sort(),\n vsCurrency,\n ].join('\\0');\n\n const existing = this.#inFlightFetches.get(inFlightKey);\n if (existing) {\n await existing;\n return;\n }\n\n // Same-key callers join this promise instead of replacing it, so cleanup\n // can always delete without checking identity.\n const fetchPromise = this.#fetchDeFiPositions(\n options,\n query,\n vsCurrency,\n ).finally(() => {\n this.#inFlightFetches.delete(inFlightKey);\n });\n this.#inFlightFetches.set(inFlightKey, fetchPromise);\n\n await fetchPromise;\n }\n\n async #fetchDeFiPositions(\n options: { forceRefresh?: boolean } | undefined,\n { networks, internalAccountIdByCaip }: DeFiBalancesQuery,\n vsCurrency: string,\n ): Promise<void> {\n if (internalAccountIdByCaip.size === 0 || networks.length === 0) {\n return;\n }\n\n const accountIds = [...internalAccountIdByCaip.keys()];\n const queryOptions = {\n networks,\n includeDeFiBalances: true,\n forceFetchDeFiPositions: true,\n includePrices: true,\n vsCurrency,\n };\n\n // Resolve once per fetch so a mid-poll remote-flag change cannot stretch\n // or shrink this poll sequence inconsistently.\n const { maxAttempts, pollInterval } = getProcessingPollConfig(\n this.messenger,\n );\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n // First attempt respects forceRefresh; later polls always bypass cache\n // so we do not spin on a stale processing snapshot.\n const fetchOptions = {\n ...(options?.forceRefresh || attempt > 0 ? { staleTime: 0 } : {}),\n };\n\n let response;\n try {\n response = await this.#apiClient.accounts.fetchV6MultiAccountBalances(\n accountIds,\n queryOptions,\n fetchOptions,\n );\n } catch (error) {\n // Soft-fail so prior state stays and the in-flight promise settles.\n console.error('Failed to fetch DeFi positions', error);\n return;\n }\n\n const stillProcessing = response.accounts.some(\n (account) => account.processingDefiPositions,\n );\n\n // Only process and write when every account is ready so a partial\n // response cannot clear or overwrite positions for accounts that are\n // still indexing.\n if (!stillProcessing) {\n const positionsByAccount = groupDeFiPositionsV6(\n response,\n internalAccountIdByCaip,\n );\n\n this.update((state) => {\n for (const [accountId, positions] of Object.entries(\n positionsByAccount,\n )) {\n state.allDeFiPositionsV2[accountId] = positions;\n }\n });\n\n // Report how many attempts were needed (only when polling was\n // required) so Sentry can inform remote-flag / default poll-limit\n // tuning without flooding on first-try successes.\n const attemptsTaken = attempt + 1;\n if (attemptsTaken > 1) {\n const multipleAttemptsError = new Error(\n `DeFiPositionsControllerV2: positions ready after ${attemptsTaken} attempt(s)`,\n );\n multipleAttemptsError.name = 'DeFiPositionsV2FetchAttempts';\n this.messenger.captureException?.(multipleAttemptsError);\n }\n return;\n }\n\n const { queryKey } =\n this.#apiClient.accounts.getV6MultiAccountBalancesQueryOptions(\n accountIds,\n queryOptions,\n fetchOptions,\n );\n await this.#apiClient.accounts.queryClient.invalidateQueries({\n queryKey,\n });\n\n const isLastAttempt = attempt >= maxAttempts - 1;\n if (isLastAttempt) {\n // Report exhausted polls so Sentry can inform remote-flag / default\n // poll-limit tuning when indexing never finishes in time.\n const multipleAttemptsError = new Error(\n `DeFiPositionsControllerV2: still processing after ${maxAttempts} attempt(s)`,\n );\n multipleAttemptsError.name = 'DeFiPositionsV2ProcessingPollExhausted';\n this.messenger.captureException?.(multipleAttemptsError);\n return;\n }\n\n await delay(pollInterval);\n }\n }\n}\n"]}
|
|
@@ -52,15 +52,14 @@ export type DeFiPositionsControllerV2Messenger = Messenger<typeof controllerName
|
|
|
52
52
|
* `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.
|
|
53
53
|
* pull-to-refresh); later processing polls always bypass the cache.
|
|
54
54
|
*
|
|
55
|
-
* When the API reports
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* in-flight fetch for that group and currency.
|
|
55
|
+
* When the API reports `processingDefiPositions` for any account, this
|
|
56
|
+
* controller polls until indexing finishes or the attempt limit is reached,
|
|
57
|
+
* and only then processes and writes state — mixed ready/processing responses
|
|
58
|
+
* leave prior state untouched. Concurrent calls for the same selected accounts
|
|
59
|
+
* and `vsCurrency` share one in-flight promise so the UI can treat the pending
|
|
60
|
+
* promise as a loading signal. Calls for a different selection or fiat currency
|
|
61
|
+
* start their own fetch and leave any prior poll running, so switching back can
|
|
62
|
+
* join an in-flight fetch for that group and currency.
|
|
64
63
|
*
|
|
65
64
|
* Processing-poll `maxAttempts` / `pollInterval` are read via
|
|
66
65
|
* {@link getProcessingPollConfig} from the `defiControllerV2` remote feature
|
|
@@ -86,19 +85,19 @@ export declare class DeFiPositionsControllerV2 extends BaseController<typeof con
|
|
|
86
85
|
state?: Partial<DeFiPositionsControllerV2State>;
|
|
87
86
|
});
|
|
88
87
|
/**
|
|
89
|
-
* Fetches DeFi positions for the selected account group. State is updated
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* stay). While any account is still indexing,
|
|
93
|
-
* method polls (invalidating the balances cache
|
|
94
|
-
* accounts are
|
|
95
|
-
* Concurrent calls for the same selected
|
|
96
|
-
* in-flight promise; calls for a different
|
|
97
|
-
* a new fetch and leave prior polls running
|
|
98
|
-
* them. When a successful ready response
|
|
99
|
-
* when polling hits the attempt limit
|
|
100
|
-
* Sentry via `messenger.captureException`
|
|
101
|
-
* `DeFiPositionsV2FetchAttempts` /
|
|
88
|
+
* Fetches DeFi positions for the selected account group. State is updated only
|
|
89
|
+
* when every selected account in the response is ready (none report
|
|
90
|
+
* `processingDefiPositions`); each account key in that response replaces that
|
|
91
|
+
* account's state (other accounts stay). While any account is still indexing,
|
|
92
|
+
* prior state is kept and the method polls (invalidating the balances cache
|
|
93
|
+
* between attempts) until all selected accounts are ready, the attempt limit
|
|
94
|
+
* is reached, or a request fails. Concurrent calls for the same selected
|
|
95
|
+
* accounts and `vsCurrency` share one in-flight promise; calls for a different
|
|
96
|
+
* selection or fiat currency start a new fetch and leave prior polls running
|
|
97
|
+
* so a later switch back can join them. When a successful ready response
|
|
98
|
+
* required more than one attempt, or when polling hits the attempt limit
|
|
99
|
+
* while still processing, reports to Sentry via `messenger.captureException`
|
|
100
|
+
* (error names `DeFiPositionsV2FetchAttempts` /
|
|
102
101
|
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
103
102
|
* No-ops when disabled or when the group has no
|
|
104
103
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeFiPositionsControllerV2.d.cts","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8DAA8D,EAAE,0CAA0C;AACxH,OAAO,EAAE,cAAc,EAAE,kCAAkC;AAC3D,OAAO,KAAK,EACV,wBAAwB,EACxB,2BAA2B,EAE5B,kCAAkC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,+BAA+B;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AACrD,OAAO,KAAK,EAAE,yCAAyC,EAAE,iDAAiD;AAK1G,OAAO,KAAK,EAAE,sCAAsC,EAAE,4DAA2D;AACjH,OAAO,KAAK,EAAE,sBAAsB,EAAE,sCAAqC;AAG3E,QAAA,MAAM,cAAc,8BAA8B,CAAC;AAYnD,MAAM,MAAM,8BAA8B,GAAG;IAC3C;;;;;;;;;;;;OAYG;IACH,kBAAkB,EAAE,sBAAsB,CAAC;CAC5C,CAAC;AAWF,eAAO,MAAM,wCAAwC,QAC/C,8BAIH,CAAC;AAEJ,MAAM,MAAM,uCAAuC,GAAG,wBAAwB,CAC5E,OAAO,cAAc,EACrB,8BAA8B,CAC/B,CAAC;AAEF,MAAM,MAAM,gCAAgC,GACxC,uCAAuC,GACvC,sCAAsC,CAAC;AAE3C,MAAM,MAAM,0CAA0C,GACpD,2BAA2B,CACzB,OAAO,cAAc,EACrB,8BAA8B,CAC/B,CAAC;AAEJ,MAAM,MAAM,+BAA+B,GACzC,0CAA0C,CAAC;AAE7C;;GAEG;AACH,MAAM,MAAM,cAAc,GACtB,8DAA8D,GAC9D,yCAAyC,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,KAAK,CAAC;AAElC,MAAM,MAAM,kCAAkC,GAAG,SAAS,CACxD,OAAO,cAAc,EACrB,gCAAgC,GAAG,cAAc,EACjD,+BAA+B,GAAG,aAAa,CAChD,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"DeFiPositionsControllerV2.d.cts","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8DAA8D,EAAE,0CAA0C;AACxH,OAAO,EAAE,cAAc,EAAE,kCAAkC;AAC3D,OAAO,KAAK,EACV,wBAAwB,EACxB,2BAA2B,EAE5B,kCAAkC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,+BAA+B;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AACrD,OAAO,KAAK,EAAE,yCAAyC,EAAE,iDAAiD;AAK1G,OAAO,KAAK,EAAE,sCAAsC,EAAE,4DAA2D;AACjH,OAAO,KAAK,EAAE,sBAAsB,EAAE,sCAAqC;AAG3E,QAAA,MAAM,cAAc,8BAA8B,CAAC;AAYnD,MAAM,MAAM,8BAA8B,GAAG;IAC3C;;;;;;;;;;;;OAYG;IACH,kBAAkB,EAAE,sBAAsB,CAAC;CAC5C,CAAC;AAWF,eAAO,MAAM,wCAAwC,QAC/C,8BAIH,CAAC;AAEJ,MAAM,MAAM,uCAAuC,GAAG,wBAAwB,CAC5E,OAAO,cAAc,EACrB,8BAA8B,CAC/B,CAAC;AAEF,MAAM,MAAM,gCAAgC,GACxC,uCAAuC,GACvC,sCAAsC,CAAC;AAE3C,MAAM,MAAM,0CAA0C,GACpD,2BAA2B,CACzB,OAAO,cAAc,EACrB,8BAA8B,CAC/B,CAAC;AAEJ,MAAM,MAAM,+BAA+B,GACzC,0CAA0C,CAAC;AAE7C;;GAEG;AACH,MAAM,MAAM,cAAc,GACtB,8DAA8D,GAC9D,yCAAyC,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,KAAK,CAAC;AAElC,MAAM,MAAM,kCAAkC,GAAG,SAAS,CACxD,OAAO,cAAc,EACrB,gCAAgC,GAAG,cAAc,EACjD,+BAA+B,GAAG,aAAa,CAChD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,yBAA0B,SAAQ,cAAc,CAC3D,OAAO,cAAc,EACrB,8BAA8B,EAC9B,kCAAkC,CACnC;;IAeC;;;;;;;OAOG;gBACS,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,aAAa,EACb,KAAK,GACN,EAAE;QACD,SAAS,EAAE,kCAAkC,CAAC;QAC9C,SAAS,EAAE,iBAAiB,CAAC;QAC7B,SAAS,EAAE,MAAM,OAAO,CAAC;QACzB,aAAa,EAAE,MAAM,MAAM,CAAC;QAC5B,KAAK,CAAC,EAAE,OAAO,CAAC,8BAA8B,CAAC,CAAC;KACjD;IAqBD;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACG,kBAAkB,CAAC,OAAO,CAAC,EAAE;QACjC,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,GAAG,OAAO,CAAC,IAAI,CAAC;CA6IlB"}
|
|
@@ -52,15 +52,14 @@ export type DeFiPositionsControllerV2Messenger = Messenger<typeof controllerName
|
|
|
52
52
|
* `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.
|
|
53
53
|
* pull-to-refresh); later processing polls always bypass the cache.
|
|
54
54
|
*
|
|
55
|
-
* When the API reports
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* in-flight fetch for that group and currency.
|
|
55
|
+
* When the API reports `processingDefiPositions` for any account, this
|
|
56
|
+
* controller polls until indexing finishes or the attempt limit is reached,
|
|
57
|
+
* and only then processes and writes state — mixed ready/processing responses
|
|
58
|
+
* leave prior state untouched. Concurrent calls for the same selected accounts
|
|
59
|
+
* and `vsCurrency` share one in-flight promise so the UI can treat the pending
|
|
60
|
+
* promise as a loading signal. Calls for a different selection or fiat currency
|
|
61
|
+
* start their own fetch and leave any prior poll running, so switching back can
|
|
62
|
+
* join an in-flight fetch for that group and currency.
|
|
64
63
|
*
|
|
65
64
|
* Processing-poll `maxAttempts` / `pollInterval` are read via
|
|
66
65
|
* {@link getProcessingPollConfig} from the `defiControllerV2` remote feature
|
|
@@ -86,19 +85,19 @@ export declare class DeFiPositionsControllerV2 extends BaseController<typeof con
|
|
|
86
85
|
state?: Partial<DeFiPositionsControllerV2State>;
|
|
87
86
|
});
|
|
88
87
|
/**
|
|
89
|
-
* Fetches DeFi positions for the selected account group. State is updated
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* stay). While any account is still indexing,
|
|
93
|
-
* method polls (invalidating the balances cache
|
|
94
|
-
* accounts are
|
|
95
|
-
* Concurrent calls for the same selected
|
|
96
|
-
* in-flight promise; calls for a different
|
|
97
|
-
* a new fetch and leave prior polls running
|
|
98
|
-
* them. When a successful ready response
|
|
99
|
-
* when polling hits the attempt limit
|
|
100
|
-
* Sentry via `messenger.captureException`
|
|
101
|
-
* `DeFiPositionsV2FetchAttempts` /
|
|
88
|
+
* Fetches DeFi positions for the selected account group. State is updated only
|
|
89
|
+
* when every selected account in the response is ready (none report
|
|
90
|
+
* `processingDefiPositions`); each account key in that response replaces that
|
|
91
|
+
* account's state (other accounts stay). While any account is still indexing,
|
|
92
|
+
* prior state is kept and the method polls (invalidating the balances cache
|
|
93
|
+
* between attempts) until all selected accounts are ready, the attempt limit
|
|
94
|
+
* is reached, or a request fails. Concurrent calls for the same selected
|
|
95
|
+
* accounts and `vsCurrency` share one in-flight promise; calls for a different
|
|
96
|
+
* selection or fiat currency start a new fetch and leave prior polls running
|
|
97
|
+
* so a later switch back can join them. When a successful ready response
|
|
98
|
+
* required more than one attempt, or when polling hits the attempt limit
|
|
99
|
+
* while still processing, reports to Sentry via `messenger.captureException`
|
|
100
|
+
* (error names `DeFiPositionsV2FetchAttempts` /
|
|
102
101
|
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
103
102
|
* No-ops when disabled or when the group has no
|
|
104
103
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeFiPositionsControllerV2.d.mts","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8DAA8D,EAAE,0CAA0C;AACxH,OAAO,EAAE,cAAc,EAAE,kCAAkC;AAC3D,OAAO,KAAK,EACV,wBAAwB,EACxB,2BAA2B,EAE5B,kCAAkC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,+BAA+B;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AACrD,OAAO,KAAK,EAAE,yCAAyC,EAAE,iDAAiD;AAK1G,OAAO,KAAK,EAAE,sCAAsC,EAAE,4DAA2D;AACjH,OAAO,KAAK,EAAE,sBAAsB,EAAE,sCAAqC;AAG3E,QAAA,MAAM,cAAc,8BAA8B,CAAC;AAYnD,MAAM,MAAM,8BAA8B,GAAG;IAC3C;;;;;;;;;;;;OAYG;IACH,kBAAkB,EAAE,sBAAsB,CAAC;CAC5C,CAAC;AAWF,eAAO,MAAM,wCAAwC,QAC/C,8BAIH,CAAC;AAEJ,MAAM,MAAM,uCAAuC,GAAG,wBAAwB,CAC5E,OAAO,cAAc,EACrB,8BAA8B,CAC/B,CAAC;AAEF,MAAM,MAAM,gCAAgC,GACxC,uCAAuC,GACvC,sCAAsC,CAAC;AAE3C,MAAM,MAAM,0CAA0C,GACpD,2BAA2B,CACzB,OAAO,cAAc,EACrB,8BAA8B,CAC/B,CAAC;AAEJ,MAAM,MAAM,+BAA+B,GACzC,0CAA0C,CAAC;AAE7C;;GAEG;AACH,MAAM,MAAM,cAAc,GACtB,8DAA8D,GAC9D,yCAAyC,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,KAAK,CAAC;AAElC,MAAM,MAAM,kCAAkC,GAAG,SAAS,CACxD,OAAO,cAAc,EACrB,gCAAgC,GAAG,cAAc,EACjD,+BAA+B,GAAG,aAAa,CAChD,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"DeFiPositionsControllerV2.d.mts","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8DAA8D,EAAE,0CAA0C;AACxH,OAAO,EAAE,cAAc,EAAE,kCAAkC;AAC3D,OAAO,KAAK,EACV,wBAAwB,EACxB,2BAA2B,EAE5B,kCAAkC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,+BAA+B;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,4BAA4B;AACrD,OAAO,KAAK,EAAE,yCAAyC,EAAE,iDAAiD;AAK1G,OAAO,KAAK,EAAE,sCAAsC,EAAE,4DAA2D;AACjH,OAAO,KAAK,EAAE,sBAAsB,EAAE,sCAAqC;AAG3E,QAAA,MAAM,cAAc,8BAA8B,CAAC;AAYnD,MAAM,MAAM,8BAA8B,GAAG;IAC3C;;;;;;;;;;;;OAYG;IACH,kBAAkB,EAAE,sBAAsB,CAAC;CAC5C,CAAC;AAWF,eAAO,MAAM,wCAAwC,QAC/C,8BAIH,CAAC;AAEJ,MAAM,MAAM,uCAAuC,GAAG,wBAAwB,CAC5E,OAAO,cAAc,EACrB,8BAA8B,CAC/B,CAAC;AAEF,MAAM,MAAM,gCAAgC,GACxC,uCAAuC,GACvC,sCAAsC,CAAC;AAE3C,MAAM,MAAM,0CAA0C,GACpD,2BAA2B,CACzB,OAAO,cAAc,EACrB,8BAA8B,CAC/B,CAAC;AAEJ,MAAM,MAAM,+BAA+B,GACzC,0CAA0C,CAAC;AAE7C;;GAEG;AACH,MAAM,MAAM,cAAc,GACtB,8DAA8D,GAC9D,yCAAyC,CAAC;AAE9C;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,KAAK,CAAC;AAElC,MAAM,MAAM,kCAAkC,GAAG,SAAS,CACxD,OAAO,cAAc,EACrB,gCAAgC,GAAG,cAAc,EACjD,+BAA+B,GAAG,aAAa,CAChD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,yBAA0B,SAAQ,cAAc,CAC3D,OAAO,cAAc,EACrB,8BAA8B,EAC9B,kCAAkC,CACnC;;IAeC;;;;;;;OAOG;gBACS,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,aAAa,EACb,KAAK,GACN,EAAE;QACD,SAAS,EAAE,kCAAkC,CAAC;QAC9C,SAAS,EAAE,iBAAiB,CAAC;QAC7B,SAAS,EAAE,MAAM,OAAO,CAAC;QACzB,aAAa,EAAE,MAAM,MAAM,CAAC;QAC5B,KAAK,CAAC,EAAE,OAAO,CAAC,8BAA8B,CAAC,CAAC;KACjD;IAqBD;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACG,kBAAkB,CAAC,OAAO,CAAC,EAAE;QACjC,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,GAAG,OAAO,CAAC,IAAI,CAAC;CA6IlB"}
|
|
@@ -46,15 +46,14 @@ export const getDefaultDeFiPositionsControllerV2State = () => {
|
|
|
46
46
|
* `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.
|
|
47
47
|
* pull-to-refresh); later processing polls always bypass the cache.
|
|
48
48
|
*
|
|
49
|
-
* When the API reports
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* in-flight fetch for that group and currency.
|
|
49
|
+
* When the API reports `processingDefiPositions` for any account, this
|
|
50
|
+
* controller polls until indexing finishes or the attempt limit is reached,
|
|
51
|
+
* and only then processes and writes state — mixed ready/processing responses
|
|
52
|
+
* leave prior state untouched. Concurrent calls for the same selected accounts
|
|
53
|
+
* and `vsCurrency` share one in-flight promise so the UI can treat the pending
|
|
54
|
+
* promise as a loading signal. Calls for a different selection or fiat currency
|
|
55
|
+
* start their own fetch and leave any prior poll running, so switching back can
|
|
56
|
+
* join an in-flight fetch for that group and currency.
|
|
58
57
|
*
|
|
59
58
|
* Processing-poll `maxAttempts` / `pollInterval` are read via
|
|
60
59
|
* {@link getProcessingPollConfig} from the `defiControllerV2` remote feature
|
|
@@ -98,19 +97,19 @@ export class DeFiPositionsControllerV2 extends BaseController {
|
|
|
98
97
|
this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
|
|
99
98
|
}
|
|
100
99
|
/**
|
|
101
|
-
* Fetches DeFi positions for the selected account group. State is updated
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* stay). While any account is still indexing,
|
|
105
|
-
* method polls (invalidating the balances cache
|
|
106
|
-
* accounts are
|
|
107
|
-
* Concurrent calls for the same selected
|
|
108
|
-
* in-flight promise; calls for a different
|
|
109
|
-
* a new fetch and leave prior polls running
|
|
110
|
-
* them. When a successful ready response
|
|
111
|
-
* when polling hits the attempt limit
|
|
112
|
-
* Sentry via `messenger.captureException`
|
|
113
|
-
* `DeFiPositionsV2FetchAttempts` /
|
|
100
|
+
* Fetches DeFi positions for the selected account group. State is updated only
|
|
101
|
+
* when every selected account in the response is ready (none report
|
|
102
|
+
* `processingDefiPositions`); each account key in that response replaces that
|
|
103
|
+
* account's state (other accounts stay). While any account is still indexing,
|
|
104
|
+
* prior state is kept and the method polls (invalidating the balances cache
|
|
105
|
+
* between attempts) until all selected accounts are ready, the attempt limit
|
|
106
|
+
* is reached, or a request fails. Concurrent calls for the same selected
|
|
107
|
+
* accounts and `vsCurrency` share one in-flight promise; calls for a different
|
|
108
|
+
* selection or fiat currency start a new fetch and leave prior polls running
|
|
109
|
+
* so a later switch back can join them. When a successful ready response
|
|
110
|
+
* required more than one attempt, or when polling hits the attempt limit
|
|
111
|
+
* while still processing, reports to Sentry via `messenger.captureException`
|
|
112
|
+
* (error names `DeFiPositionsV2FetchAttempts` /
|
|
114
113
|
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
115
114
|
* No-ops when disabled or when the group has no
|
|
116
115
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
@@ -180,10 +179,10 @@ _DeFiPositionsControllerV2_apiClient = new WeakMap(), _DeFiPositionsControllerV2
|
|
|
180
179
|
console.error('Failed to fetch DeFi positions', error);
|
|
181
180
|
return;
|
|
182
181
|
}
|
|
183
|
-
const stillProcessing =
|
|
184
|
-
// Only process and write when
|
|
185
|
-
//
|
|
186
|
-
//
|
|
182
|
+
const stillProcessing = response.accounts.some((account) => account.processingDefiPositions);
|
|
183
|
+
// Only process and write when every account is ready so a partial
|
|
184
|
+
// response cannot clear or overwrite positions for accounts that are
|
|
185
|
+
// still indexing.
|
|
187
186
|
if (!stillProcessing) {
|
|
188
187
|
const positionsByAccount = groupDeFiPositionsV6(response, internalAccountIdByCaip);
|
|
189
188
|
this.update((state) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeFiPositionsControllerV2.mjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccountTreeControllerGetAccountsFromSelectedAccountGroupAction } from '@metamask/account-tree-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n ControllerGetStateAction,\n ControllerStateChangedEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport type { ApiPlatformClient } from '@metamask/core-backend';\nimport type { Messenger } from '@metamask/messenger';\nimport type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';\n\nimport { buildDeFiBalancesQuery } from './build-defi-balances-query.js';\nimport type { DeFiBalancesQuery } from './build-defi-balances-query.js';\nimport { getProcessingPollConfig } from './defi-controller-v2-feature-flag.js';\nimport type { DeFiPositionsControllerV2MethodActions } from './DeFiPositionsControllerV2-method-action-types.js';\nimport type { DeFiPositionsByAccount } from './group-defi-positions-v6.js';\nimport { groupDeFiPositionsV6 } from './group-defi-positions-v6.js';\n\nconst controllerName = 'DeFiPositionsControllerV2';\n\nconst MESSENGER_EXPOSED_METHODS = ['fetchDeFiPositions'] as const;\n\n/**\n * @param ms - Milliseconds to wait.\n * @returns A promise that resolves after `ms`.\n */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport type DeFiPositionsControllerV2State = {\n /**\n * DeFi positions keyed by internal MetaMask account ID (`InternalAccount.id`,\n * the same key AssetsController uses). Each account maps to a flat list of\n * protocol groups shown in the DeFi tab, each carrying its own `chainId` for\n * filtering plus the details-page sections embedded inside it. This is\n * exactly the shape the client consumes, so no further transformation is\n * needed on read.\n *\n * Named `allDeFiPositionsV2` (rather than `allDeFiPositions`) so it can live\n * alongside the legacy `DeFiPositionsController` in clients that flatten every\n * controller's state into a single object (e.g. the extension background),\n * without colliding on the shared `allDeFiPositions` key.\n */\n allDeFiPositionsV2: DeFiPositionsByAccount;\n};\n\nconst controllerMetadata: StateMetadata<DeFiPositionsControllerV2State> = {\n allDeFiPositionsV2: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n};\n\nexport const getDefaultDeFiPositionsControllerV2State =\n (): DeFiPositionsControllerV2State => {\n return {\n allDeFiPositionsV2: {},\n };\n };\n\nexport type DeFiPositionsControllerV2GetStateAction = ControllerGetStateAction<\n typeof controllerName,\n DeFiPositionsControllerV2State\n>;\n\nexport type DeFiPositionsControllerV2Actions =\n | DeFiPositionsControllerV2GetStateAction\n | DeFiPositionsControllerV2MethodActions;\n\nexport type DeFiPositionsControllerV2StateChangedEvent =\n ControllerStateChangedEvent<\n typeof controllerName,\n DeFiPositionsControllerV2State\n >;\n\nexport type DeFiPositionsControllerV2Events =\n DeFiPositionsControllerV2StateChangedEvent;\n\n/**\n * The external actions available to the {@link DeFiPositionsControllerV2}.\n */\nexport type AllowedActions =\n | AccountTreeControllerGetAccountsFromSelectedAccountGroupAction\n | RemoteFeatureFlagControllerGetStateAction;\n\n/**\n * The external events available to the {@link DeFiPositionsControllerV2}.\n *\n * None yet — clients must call `fetchDeFiPositions` (and optionally\n * `{ forceRefresh: true }`) on their own triggers. Likely future subscriptions:\n * `AccountTreeController:selectedAccountGroupChange`,\n * `TransactionController:transactionConfirmed`, and `KeyringController:lock`.\n */\nexport type AllowedEvents = never;\n\nexport type DeFiPositionsControllerV2Messenger = Messenger<\n typeof controllerName,\n DeFiPositionsControllerV2Actions | AllowedActions,\n DeFiPositionsControllerV2Events | AllowedEvents\n>;\n\n/**\n * Controller that fetches DeFi positions for the selected account group from\n * the Accounts API (v6 multiaccount balances) and stores them in the shape the\n * client consumes directly.\n *\n * Deduplication and freshness are handled by the shared TanStack Query cache on\n * {@link ApiPlatformClient} (balances default `staleTime` is 1 minute). Pass\n * `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.\n * pull-to-refresh); later processing polls always bypass the cache.\n *\n * When the API reports account IDs in the response-level\n * `processingDefiPositions` array, this controller polls until indexing\n * finishes or the attempt limit is reached, and only then processes and writes\n * state — responses that still list processing accounts leave prior state\n * untouched. Concurrent calls for the same selected accounts and `vsCurrency`\n * share one in-flight promise so the UI can treat the pending promise as a\n * loading signal. Calls for a different selection or fiat currency start their\n * own fetch and leave any prior poll running, so switching back can join an\n * in-flight fetch for that group and currency.\n *\n * Processing-poll `maxAttempts` / `pollInterval` are read via\n * {@link getProcessingPollConfig} from the `defiControllerV2` remote feature\n * flag (`RemoteFeatureFlagController:getState`), falling back to built-in\n * defaults when unset or invalid. Clients must delegate that action to this\n * controller's messenger.\n */\nexport class DeFiPositionsControllerV2 extends BaseController<\n typeof controllerName,\n DeFiPositionsControllerV2State,\n DeFiPositionsControllerV2Messenger\n> {\n readonly #apiClient: ApiPlatformClient;\n\n readonly #isEnabled: () => boolean;\n\n readonly #getVsCurrency: () => string;\n\n /**\n * In-flight fetches keyed by selected DeFi-queryable account IDs plus\n * `vsCurrency`. Concurrent callers for the same selection and currency share\n * a promise; different selections or fiat currencies keep independent\n * fetches so fast switching can join an earlier matching poll.\n */\n readonly #inFlightFetches = new Map<string, Promise<void>>();\n\n /**\n * @param options - Constructor options.\n * @param options.messenger - The controller messenger.\n * @param options.apiClient - Accounts API client used to fetch balances/positions. Auth is handled by the client.\n * @param options.isEnabled - Returns whether fetching is enabled (default: () => false).\n * @param options.getVsCurrency - Returns the fiat currency for prices (default: () => 'usd').\n * @param options.state - Initial controller state.\n */\n constructor({\n messenger,\n apiClient,\n isEnabled,\n getVsCurrency,\n state,\n }: {\n messenger: DeFiPositionsControllerV2Messenger;\n apiClient: ApiPlatformClient;\n isEnabled: () => boolean;\n getVsCurrency: () => string;\n state?: Partial<DeFiPositionsControllerV2State>;\n }) {\n super({\n name: controllerName,\n metadata: controllerMetadata,\n messenger,\n state: {\n ...getDefaultDeFiPositionsControllerV2State(),\n ...state,\n },\n });\n\n this.#apiClient = apiClient;\n this.#isEnabled = isEnabled;\n this.#getVsCurrency = getVsCurrency;\n\n this.messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Fetches DeFi positions for the selected account group. State is updated\n * only when the response lists no accounts in its `processingDefiPositions`\n * array; each resolved account then has its state replaced (other accounts\n * stay). While any account is still indexing, prior state is kept and the\n * method polls (invalidating the balances cache between attempts) until no\n * accounts are processing, the attempt limit is reached, or a request fails.\n * Concurrent calls for the same selected accounts and `vsCurrency` share one\n * in-flight promise; calls for a different selection or fiat currency start\n * a new fetch and leave prior polls running so a later switch back can join\n * them. When a successful ready response required more than one attempt, or\n * when polling hits the attempt limit while still processing, reports to\n * Sentry via `messenger.captureException` (error names\n * `DeFiPositionsV2FetchAttempts` /\n * `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.\n * No-ops when disabled or when the group has no\n * supported accounts. Caching / spam prevention is handled by the apiClient\n * TanStack Query cache (keyed by accounts + query options including\n * `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the\n * first attempt (e.g. pull-to-refresh).\n *\n * @param options - Optional fetch modifiers.\n * @param options.forceRefresh - When true, bypass the apiClient cache on the\n * first attempt and fetch immediately.\n * @returns Resolves when the fetch (and any processing polls) finish.\n */\n async fetchDeFiPositions(options?: {\n forceRefresh?: boolean;\n }): Promise<void> {\n if (!this.#isEnabled()) {\n return;\n }\n\n const selectedAccounts = this.messenger.call(\n 'AccountTreeController:getAccountsFromSelectedAccountGroup',\n );\n const query = buildDeFiBalancesQuery(selectedAccounts);\n const vsCurrency = this.#getVsCurrency().toLowerCase();\n // Include vsCurrency so a fiat change does not join a poll priced in the\n // previous currency (TanStack also keys the balances cache on vsCurrency).\n const inFlightKey = [\n ...[...query.internalAccountIdByCaip.keys()].sort(),\n vsCurrency,\n ].join('\\0');\n\n const existing = this.#inFlightFetches.get(inFlightKey);\n if (existing) {\n await existing;\n return;\n }\n\n // Same-key callers join this promise instead of replacing it, so cleanup\n // can always delete without checking identity.\n const fetchPromise = this.#fetchDeFiPositions(\n options,\n query,\n vsCurrency,\n ).finally(() => {\n this.#inFlightFetches.delete(inFlightKey);\n });\n this.#inFlightFetches.set(inFlightKey, fetchPromise);\n\n await fetchPromise;\n }\n\n async #fetchDeFiPositions(\n options: { forceRefresh?: boolean } | undefined,\n { networks, internalAccountIdByCaip }: DeFiBalancesQuery,\n vsCurrency: string,\n ): Promise<void> {\n if (internalAccountIdByCaip.size === 0 || networks.length === 0) {\n return;\n }\n\n const accountIds = [...internalAccountIdByCaip.keys()];\n const queryOptions = {\n networks,\n includeDeFiBalances: true,\n forceFetchDeFiPositions: true,\n includePrices: true,\n vsCurrency,\n };\n\n // Resolve once per fetch so a mid-poll remote-flag change cannot stretch\n // or shrink this poll sequence inconsistently.\n const { maxAttempts, pollInterval } = getProcessingPollConfig(\n this.messenger,\n );\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n // First attempt respects forceRefresh; later polls always bypass cache\n // so we do not spin on a stale processing snapshot.\n const fetchOptions = {\n ...(options?.forceRefresh || attempt > 0 ? { staleTime: 0 } : {}),\n };\n\n let response;\n try {\n response = await this.#apiClient.accounts.fetchV6MultiAccountBalances(\n accountIds,\n queryOptions,\n fetchOptions,\n );\n } catch (error) {\n // Soft-fail so prior state stays and the in-flight promise settles.\n console.error('Failed to fetch DeFi positions', error);\n return;\n }\n\n const stillProcessing =\n (response.processingDefiPositions?.length ?? 0) > 0;\n\n // Only process and write when no account is still indexing, so a\n // partial response (whose DeFi rows for processing accounts are\n // omitted) cannot clear or overwrite positions for those accounts.\n if (!stillProcessing) {\n const positionsByAccount = groupDeFiPositionsV6(\n response,\n internalAccountIdByCaip,\n );\n\n this.update((state) => {\n for (const [accountId, positions] of Object.entries(\n positionsByAccount,\n )) {\n state.allDeFiPositionsV2[accountId] = positions;\n }\n });\n\n // Report how many attempts were needed (only when polling was\n // required) so Sentry can inform remote-flag / default poll-limit\n // tuning without flooding on first-try successes.\n const attemptsTaken = attempt + 1;\n if (attemptsTaken > 1) {\n const multipleAttemptsError = new Error(\n `DeFiPositionsControllerV2: positions ready after ${attemptsTaken} attempt(s)`,\n );\n multipleAttemptsError.name = 'DeFiPositionsV2FetchAttempts';\n this.messenger.captureException?.(multipleAttemptsError);\n }\n return;\n }\n\n const { queryKey } =\n this.#apiClient.accounts.getV6MultiAccountBalancesQueryOptions(\n accountIds,\n queryOptions,\n fetchOptions,\n );\n await this.#apiClient.accounts.queryClient.invalidateQueries({\n queryKey,\n });\n\n const isLastAttempt = attempt >= maxAttempts - 1;\n if (isLastAttempt) {\n // Report exhausted polls so Sentry can inform remote-flag / default\n // poll-limit tuning when indexing never finishes in time.\n const multipleAttemptsError = new Error(\n `DeFiPositionsControllerV2: still processing after ${maxAttempts} attempt(s)`,\n );\n multipleAttemptsError.name = 'DeFiPositionsV2ProcessingPollExhausted';\n this.messenger.captureException?.(multipleAttemptsError);\n return;\n }\n\n await delay(pollInterval);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"DeFiPositionsControllerV2.mjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccountTreeControllerGetAccountsFromSelectedAccountGroupAction } from '@metamask/account-tree-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport type {\n ControllerGetStateAction,\n ControllerStateChangedEvent,\n StateMetadata,\n} from '@metamask/base-controller';\nimport type { ApiPlatformClient } from '@metamask/core-backend';\nimport type { Messenger } from '@metamask/messenger';\nimport type { RemoteFeatureFlagControllerGetStateAction } from '@metamask/remote-feature-flag-controller';\n\nimport { buildDeFiBalancesQuery } from './build-defi-balances-query.js';\nimport type { DeFiBalancesQuery } from './build-defi-balances-query.js';\nimport { getProcessingPollConfig } from './defi-controller-v2-feature-flag.js';\nimport type { DeFiPositionsControllerV2MethodActions } from './DeFiPositionsControllerV2-method-action-types.js';\nimport type { DeFiPositionsByAccount } from './group-defi-positions-v6.js';\nimport { groupDeFiPositionsV6 } from './group-defi-positions-v6.js';\n\nconst controllerName = 'DeFiPositionsControllerV2';\n\nconst MESSENGER_EXPOSED_METHODS = ['fetchDeFiPositions'] as const;\n\n/**\n * @param ms - Milliseconds to wait.\n * @returns A promise that resolves after `ms`.\n */\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport type DeFiPositionsControllerV2State = {\n /**\n * DeFi positions keyed by internal MetaMask account ID (`InternalAccount.id`,\n * the same key AssetsController uses). Each account maps to a flat list of\n * protocol groups shown in the DeFi tab, each carrying its own `chainId` for\n * filtering plus the details-page sections embedded inside it. This is\n * exactly the shape the client consumes, so no further transformation is\n * needed on read.\n *\n * Named `allDeFiPositionsV2` (rather than `allDeFiPositions`) so it can live\n * alongside the legacy `DeFiPositionsController` in clients that flatten every\n * controller's state into a single object (e.g. the extension background),\n * without colliding on the shared `allDeFiPositions` key.\n */\n allDeFiPositionsV2: DeFiPositionsByAccount;\n};\n\nconst controllerMetadata: StateMetadata<DeFiPositionsControllerV2State> = {\n allDeFiPositionsV2: {\n includeInStateLogs: false,\n persist: true,\n includeInDebugSnapshot: false,\n usedInUi: true,\n },\n};\n\nexport const getDefaultDeFiPositionsControllerV2State =\n (): DeFiPositionsControllerV2State => {\n return {\n allDeFiPositionsV2: {},\n };\n };\n\nexport type DeFiPositionsControllerV2GetStateAction = ControllerGetStateAction<\n typeof controllerName,\n DeFiPositionsControllerV2State\n>;\n\nexport type DeFiPositionsControllerV2Actions =\n | DeFiPositionsControllerV2GetStateAction\n | DeFiPositionsControllerV2MethodActions;\n\nexport type DeFiPositionsControllerV2StateChangedEvent =\n ControllerStateChangedEvent<\n typeof controllerName,\n DeFiPositionsControllerV2State\n >;\n\nexport type DeFiPositionsControllerV2Events =\n DeFiPositionsControllerV2StateChangedEvent;\n\n/**\n * The external actions available to the {@link DeFiPositionsControllerV2}.\n */\nexport type AllowedActions =\n | AccountTreeControllerGetAccountsFromSelectedAccountGroupAction\n | RemoteFeatureFlagControllerGetStateAction;\n\n/**\n * The external events available to the {@link DeFiPositionsControllerV2}.\n *\n * None yet — clients must call `fetchDeFiPositions` (and optionally\n * `{ forceRefresh: true }`) on their own triggers. Likely future subscriptions:\n * `AccountTreeController:selectedAccountGroupChange`,\n * `TransactionController:transactionConfirmed`, and `KeyringController:lock`.\n */\nexport type AllowedEvents = never;\n\nexport type DeFiPositionsControllerV2Messenger = Messenger<\n typeof controllerName,\n DeFiPositionsControllerV2Actions | AllowedActions,\n DeFiPositionsControllerV2Events | AllowedEvents\n>;\n\n/**\n * Controller that fetches DeFi positions for the selected account group from\n * the Accounts API (v6 multiaccount balances) and stores them in the shape the\n * client consumes directly.\n *\n * Deduplication and freshness are handled by the shared TanStack Query cache on\n * {@link ApiPlatformClient} (balances default `staleTime` is 1 minute). Pass\n * `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.\n * pull-to-refresh); later processing polls always bypass the cache.\n *\n * When the API reports `processingDefiPositions` for any account, this\n * controller polls until indexing finishes or the attempt limit is reached,\n * and only then processes and writes state — mixed ready/processing responses\n * leave prior state untouched. Concurrent calls for the same selected accounts\n * and `vsCurrency` share one in-flight promise so the UI can treat the pending\n * promise as a loading signal. Calls for a different selection or fiat currency\n * start their own fetch and leave any prior poll running, so switching back can\n * join an in-flight fetch for that group and currency.\n *\n * Processing-poll `maxAttempts` / `pollInterval` are read via\n * {@link getProcessingPollConfig} from the `defiControllerV2` remote feature\n * flag (`RemoteFeatureFlagController:getState`), falling back to built-in\n * defaults when unset or invalid. Clients must delegate that action to this\n * controller's messenger.\n */\nexport class DeFiPositionsControllerV2 extends BaseController<\n typeof controllerName,\n DeFiPositionsControllerV2State,\n DeFiPositionsControllerV2Messenger\n> {\n readonly #apiClient: ApiPlatformClient;\n\n readonly #isEnabled: () => boolean;\n\n readonly #getVsCurrency: () => string;\n\n /**\n * In-flight fetches keyed by selected DeFi-queryable account IDs plus\n * `vsCurrency`. Concurrent callers for the same selection and currency share\n * a promise; different selections or fiat currencies keep independent\n * fetches so fast switching can join an earlier matching poll.\n */\n readonly #inFlightFetches = new Map<string, Promise<void>>();\n\n /**\n * @param options - Constructor options.\n * @param options.messenger - The controller messenger.\n * @param options.apiClient - Accounts API client used to fetch balances/positions. Auth is handled by the client.\n * @param options.isEnabled - Returns whether fetching is enabled (default: () => false).\n * @param options.getVsCurrency - Returns the fiat currency for prices (default: () => 'usd').\n * @param options.state - Initial controller state.\n */\n constructor({\n messenger,\n apiClient,\n isEnabled,\n getVsCurrency,\n state,\n }: {\n messenger: DeFiPositionsControllerV2Messenger;\n apiClient: ApiPlatformClient;\n isEnabled: () => boolean;\n getVsCurrency: () => string;\n state?: Partial<DeFiPositionsControllerV2State>;\n }) {\n super({\n name: controllerName,\n metadata: controllerMetadata,\n messenger,\n state: {\n ...getDefaultDeFiPositionsControllerV2State(),\n ...state,\n },\n });\n\n this.#apiClient = apiClient;\n this.#isEnabled = isEnabled;\n this.#getVsCurrency = getVsCurrency;\n\n this.messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n }\n\n /**\n * Fetches DeFi positions for the selected account group. State is updated only\n * when every selected account in the response is ready (none report\n * `processingDefiPositions`); each account key in that response replaces that\n * account's state (other accounts stay). While any account is still indexing,\n * prior state is kept and the method polls (invalidating the balances cache\n * between attempts) until all selected accounts are ready, the attempt limit\n * is reached, or a request fails. Concurrent calls for the same selected\n * accounts and `vsCurrency` share one in-flight promise; calls for a different\n * selection or fiat currency start a new fetch and leave prior polls running\n * so a later switch back can join them. When a successful ready response\n * required more than one attempt, or when polling hits the attempt limit\n * while still processing, reports to Sentry via `messenger.captureException`\n * (error names `DeFiPositionsV2FetchAttempts` /\n * `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.\n * No-ops when disabled or when the group has no\n * supported accounts. Caching / spam prevention is handled by the apiClient\n * TanStack Query cache (keyed by accounts + query options including\n * `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the\n * first attempt (e.g. pull-to-refresh).\n *\n * @param options - Optional fetch modifiers.\n * @param options.forceRefresh - When true, bypass the apiClient cache on the\n * first attempt and fetch immediately.\n * @returns Resolves when the fetch (and any processing polls) finish.\n */\n async fetchDeFiPositions(options?: {\n forceRefresh?: boolean;\n }): Promise<void> {\n if (!this.#isEnabled()) {\n return;\n }\n\n const selectedAccounts = this.messenger.call(\n 'AccountTreeController:getAccountsFromSelectedAccountGroup',\n );\n const query = buildDeFiBalancesQuery(selectedAccounts);\n const vsCurrency = this.#getVsCurrency().toLowerCase();\n // Include vsCurrency so a fiat change does not join a poll priced in the\n // previous currency (TanStack also keys the balances cache on vsCurrency).\n const inFlightKey = [\n ...[...query.internalAccountIdByCaip.keys()].sort(),\n vsCurrency,\n ].join('\\0');\n\n const existing = this.#inFlightFetches.get(inFlightKey);\n if (existing) {\n await existing;\n return;\n }\n\n // Same-key callers join this promise instead of replacing it, so cleanup\n // can always delete without checking identity.\n const fetchPromise = this.#fetchDeFiPositions(\n options,\n query,\n vsCurrency,\n ).finally(() => {\n this.#inFlightFetches.delete(inFlightKey);\n });\n this.#inFlightFetches.set(inFlightKey, fetchPromise);\n\n await fetchPromise;\n }\n\n async #fetchDeFiPositions(\n options: { forceRefresh?: boolean } | undefined,\n { networks, internalAccountIdByCaip }: DeFiBalancesQuery,\n vsCurrency: string,\n ): Promise<void> {\n if (internalAccountIdByCaip.size === 0 || networks.length === 0) {\n return;\n }\n\n const accountIds = [...internalAccountIdByCaip.keys()];\n const queryOptions = {\n networks,\n includeDeFiBalances: true,\n forceFetchDeFiPositions: true,\n includePrices: true,\n vsCurrency,\n };\n\n // Resolve once per fetch so a mid-poll remote-flag change cannot stretch\n // or shrink this poll sequence inconsistently.\n const { maxAttempts, pollInterval } = getProcessingPollConfig(\n this.messenger,\n );\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n // First attempt respects forceRefresh; later polls always bypass cache\n // so we do not spin on a stale processing snapshot.\n const fetchOptions = {\n ...(options?.forceRefresh || attempt > 0 ? { staleTime: 0 } : {}),\n };\n\n let response;\n try {\n response = await this.#apiClient.accounts.fetchV6MultiAccountBalances(\n accountIds,\n queryOptions,\n fetchOptions,\n );\n } catch (error) {\n // Soft-fail so prior state stays and the in-flight promise settles.\n console.error('Failed to fetch DeFi positions', error);\n return;\n }\n\n const stillProcessing = response.accounts.some(\n (account) => account.processingDefiPositions,\n );\n\n // Only process and write when every account is ready so a partial\n // response cannot clear or overwrite positions for accounts that are\n // still indexing.\n if (!stillProcessing) {\n const positionsByAccount = groupDeFiPositionsV6(\n response,\n internalAccountIdByCaip,\n );\n\n this.update((state) => {\n for (const [accountId, positions] of Object.entries(\n positionsByAccount,\n )) {\n state.allDeFiPositionsV2[accountId] = positions;\n }\n });\n\n // Report how many attempts were needed (only when polling was\n // required) so Sentry can inform remote-flag / default poll-limit\n // tuning without flooding on first-try successes.\n const attemptsTaken = attempt + 1;\n if (attemptsTaken > 1) {\n const multipleAttemptsError = new Error(\n `DeFiPositionsControllerV2: positions ready after ${attemptsTaken} attempt(s)`,\n );\n multipleAttemptsError.name = 'DeFiPositionsV2FetchAttempts';\n this.messenger.captureException?.(multipleAttemptsError);\n }\n return;\n }\n\n const { queryKey } =\n this.#apiClient.accounts.getV6MultiAccountBalancesQueryOptions(\n accountIds,\n queryOptions,\n fetchOptions,\n );\n await this.#apiClient.accounts.queryClient.invalidateQueries({\n queryKey,\n });\n\n const isLastAttempt = attempt >= maxAttempts - 1;\n if (isLastAttempt) {\n // Report exhausted polls so Sentry can inform remote-flag / default\n // poll-limit tuning when indexing never finishes in time.\n const multipleAttemptsError = new Error(\n `DeFiPositionsControllerV2: still processing after ${maxAttempts} attempt(s)`,\n );\n multipleAttemptsError.name = 'DeFiPositionsV2ProcessingPollExhausted';\n this.messenger.captureException?.(multipleAttemptsError);\n return;\n }\n\n await delay(pollInterval);\n }\n }\n}\n"]}
|
|
@@ -40,10 +40,10 @@ function getDefiTokenImageUrl(assetId) {
|
|
|
40
40
|
* Returns whether a balance row is a DeFi position carrying protocol metadata.
|
|
41
41
|
*
|
|
42
42
|
* @param balance - A balance row from the v6 API.
|
|
43
|
-
* @returns True when the row is
|
|
43
|
+
* @returns True when the row is a `category: defi` row with protocol metadata.
|
|
44
44
|
*/
|
|
45
45
|
function isDefiBalanceWithMetadata(balance) {
|
|
46
|
-
return (balance.
|
|
46
|
+
return (balance.category === 'defi' &&
|
|
47
47
|
balance.metadata !== undefined &&
|
|
48
48
|
balance.metadata.protocolId !== undefined);
|
|
49
49
|
}
|
|
@@ -142,9 +142,8 @@ function toAccountMatchKey(caipAccountId) {
|
|
|
142
142
|
* positions keyed by internal account ID, each mapping to a flat list of
|
|
143
143
|
* protocol groups. Every group carries its own `chainId` (so the client can
|
|
144
144
|
* filter without a nested chain map) plus both the DeFi-tab summary and the
|
|
145
|
-
* details-page sections.
|
|
146
|
-
*
|
|
147
|
-
* even when the flat response contains no rows for that account.
|
|
145
|
+
* details-page sections. Accounts present in the response but with no DeFi
|
|
146
|
+
* positions are included with an empty list so stale data is cleared.
|
|
148
147
|
*
|
|
149
148
|
* When `internalAccountIdByCaip` is provided, response account IDs are matched
|
|
150
149
|
* to internal MetaMask account IDs via namespace + address (ignoring chain
|
|
@@ -163,67 +162,64 @@ function groupDeFiPositionsV6(response, internalAccountIdByCaip) {
|
|
|
163
162
|
internalId,
|
|
164
163
|
]))
|
|
165
164
|
: undefined;
|
|
166
|
-
// Accumulate groups per resolved internal account ID. The v6 response
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
const groupsByAccountKey = new Map(
|
|
171
|
-
|
|
172
|
-
accountId,
|
|
173
|
-
new Map(),
|
|
174
|
-
])
|
|
175
|
-
: []);
|
|
176
|
-
for (const balance of response.balances) {
|
|
165
|
+
// Accumulate groups per resolved internal account ID. The v6 response returns
|
|
166
|
+
// a separate entry per chain (e.g. `eip155:1:<addr>`, `eip155:137:<addr>`),
|
|
167
|
+
// and several of them can resolve to the same internal account ID, so we must
|
|
168
|
+
// merge across all of them rather than overwrite per response account.
|
|
169
|
+
const groupsByAccountKey = new Map();
|
|
170
|
+
for (const account of response.accounts) {
|
|
177
171
|
const accountId = internalAccountIdByMatchKey
|
|
178
|
-
? internalAccountIdByMatchKey.get(toAccountMatchKey(
|
|
179
|
-
:
|
|
172
|
+
? internalAccountIdByMatchKey.get(toAccountMatchKey(account.accountId))
|
|
173
|
+
: account.accountId;
|
|
180
174
|
if (accountId === undefined) {
|
|
181
175
|
continue;
|
|
182
176
|
}
|
|
177
|
+
// Seed every queried account so accounts that no longer hold positions
|
|
178
|
+
// overwrite (clear) any previously stored data.
|
|
183
179
|
let groupsByKey = groupsByAccountKey.get(accountId);
|
|
184
180
|
if (!groupsByKey) {
|
|
185
181
|
groupsByKey = new Map();
|
|
186
182
|
groupsByAccountKey.set(accountId, groupsByKey);
|
|
187
183
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
group
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
184
|
+
for (const balance of account.balances) {
|
|
185
|
+
if (!isDefiBalanceWithMetadata(balance)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const position = toUnderlyingPosition(balance);
|
|
189
|
+
const { protocolId, productName, protocolIconUrl } = balance.metadata;
|
|
190
|
+
const groupKey = `${position.chainId}#${protocolId}`;
|
|
191
|
+
let group = groupsByKey.get(groupKey);
|
|
192
|
+
if (!group) {
|
|
193
|
+
group = {
|
|
194
|
+
protocolId,
|
|
195
|
+
productName,
|
|
196
|
+
protocolIconUrl,
|
|
197
|
+
chainId: position.chainId,
|
|
198
|
+
marketValue: 0,
|
|
199
|
+
iconGroup: [],
|
|
200
|
+
sections: [],
|
|
201
|
+
};
|
|
202
|
+
groupsByKey.set(groupKey, group);
|
|
203
|
+
}
|
|
204
|
+
if (position.marketValue !== undefined) {
|
|
205
|
+
group.marketValue +=
|
|
206
|
+
position.marketValue * getMarketValueSign(position.positionType);
|
|
207
|
+
}
|
|
208
|
+
if (!group.iconGroup.some((item) => item.symbol === position.symbol)) {
|
|
209
|
+
group.iconGroup.push({
|
|
210
|
+
symbol: position.symbol,
|
|
211
|
+
avatarValue: position.tokenImage,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Sections are keyed by productName; distinct groupIds under the same
|
|
215
|
+
// product remain available on each underlying position.
|
|
216
|
+
let section = group.sections.find((item) => item.productName === productName);
|
|
217
|
+
if (!section) {
|
|
218
|
+
section = { productName, positions: [] };
|
|
219
|
+
group.sections.push(section);
|
|
220
|
+
}
|
|
221
|
+
section.positions.push(position);
|
|
225
222
|
}
|
|
226
|
-
section.positions.push(position);
|
|
227
223
|
}
|
|
228
224
|
const result = {};
|
|
229
225
|
for (const [accountId, groupsByKey] of groupsByAccountKey) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-defi-positions-v6.cjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/group-defi-positions-v6.ts"],"names":[],"mappings":"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","sourcesContent":["import { V6_DEFI_POSITION_TYPES } from '@metamask/core-backend';\nimport type {\n V6BalanceItem,\n V6BalanceMetadata,\n V6BalancesResponse,\n V6DeFiPositionType,\n} from '@metamask/core-backend';\nimport type {\n CaipAccountId,\n CaipAssetType,\n CaipChainId,\n} from '@metamask/utils';\nimport {\n KnownCaipNamespace,\n parseCaipAccountId,\n parseCaipAssetType,\n parseCaipChainId,\n} from '@metamask/utils';\n\n/** Static.cx host used to build CAIP-19 token icon URLs for DeFi positions. */\nconst STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io';\n\n/**\n * Possible `positionType` values from Accounts API v6 DeFi metadata.\n * Re-export of {@link V6_DEFI_POSITION_TYPES} from `@metamask/core-backend`.\n */\nexport const DEFI_POSITION_TYPES = V6_DEFI_POSITION_TYPES;\n\n/**\n * The specific module or functionality within a DeFi protocol where a position\n * is held. Alias of {@link V6DeFiPositionType} from `@metamask/core-backend`.\n */\nexport type DeFiPositionType = V6DeFiPositionType;\n\n/**\n * Position types whose fiat value is a liability and is subtracted from the\n * protocol group's aggregated `marketValue`.\n */\nexport const DEFI_POSITION_LIABILITY_TYPES: ReadonlySet<DeFiPositionType> =\n new Set<DeFiPositionType>(['loan']);\n\n/**\n * An icon-group entry shown next to a protocol in the DeFi tab list.\n */\nexport type DeFiPositionIconGroupItem = {\n /** Token icon URL, when one can be built for the asset. */\n avatarValue?: string;\n symbol: string;\n};\n\n/**\n * A single underlying position row shown on the DeFi details page.\n */\nexport type DeFiUnderlyingPosition = {\n assetId: CaipAssetType;\n chainId: CaipChainId;\n symbol: string;\n name: string;\n decimals: number;\n /** Raw balance string as returned by the API. */\n balance: string;\n /** Fiat market value in the requested currency, when a price is available. */\n marketValue?: number;\n /** Position type from protocol metadata. */\n positionType: DeFiPositionType;\n /** Address of the pool this position belongs to. */\n poolAddress: string;\n /**\n * Upstream grouping id from the API. Rows that share a `productName` can\n * still carry distinct `groupId`s (e.g. multiple Pendle YT markets).\n */\n groupId: string;\n /** Token icon URL, when one can be built for the asset. */\n tokenImage?: string;\n};\n\n/**\n * A section of the details page, grouping positions that share the same\n * API `productName`. A single `protocolId` can have multiple products\n * (different pools/markets under one protocol), so a group may contain\n * several sections.\n */\nexport type DeFiPositionDetailsSection = {\n /** Section label from the API (`metadata.productName`). */\n productName: string;\n positions: DeFiUnderlyingPosition[];\n};\n\n/**\n * One row in the DeFi tab list (a protocol on a given chain), with the details\n * needed to render the details page embedded directly inside it.\n */\nexport type DeFiProtocolPositionGroup = {\n protocolId: string;\n /**\n * Product name from the first position seen for this protocol. Prefer\n * `protocolId` for the list-row title; use section `productName`s for\n * per-product detail headings.\n */\n productName: string;\n protocolIconUrl: string;\n chainId: CaipChainId;\n /**\n * Aggregated fiat market value across all positions in the group.\n * `lending` positions are subtracted; all other types are added.\n */\n marketValue: number;\n /** Icon-group entries for the list row. */\n iconGroup: DeFiPositionIconGroupItem[];\n /**\n * Detail sections consumed by the details page, one per distinct API\n * `productName` under this `protocolId`.\n */\n sections: DeFiPositionDetailsSection[];\n};\n\n/**\n * DeFi positions for every queried account, keyed by the internal MetaMask\n * account ID (`InternalAccount.id` UUID), the same key AssetsController uses.\n * Each account maps to a flat list of protocol groups; filter by each group's\n * `chainId` rather than digging through a nested chain map.\n */\nexport type DeFiPositionsByAccount = {\n [accountId: string]: DeFiProtocolPositionGroup[];\n};\n\n// Prefer ETH/WETH first in the list-row icon stack when a protocol has multiple\n// underlyings. Display-only.\nconst SYMBOL_PRIORITY = ['ETH', 'WETH'];\n\ntype DefiBalanceWithMetadata = V6BalanceItem & { metadata: V6BalanceMetadata };\n\n/**\n * Builds a static token icon URL for a CAIP asset ID.\n *\n * @param assetId - The CAIP-19 asset ID.\n * @returns The token icon URL, or `undefined` when it cannot be built.\n */\nfunction getDefiTokenImageUrl(assetId: CaipAssetType): string | undefined {\n try {\n const { chainId } = parseCaipAssetType(assetId);\n const { namespace } = parseCaipChainId(chainId);\n const isEvm = namespace === KnownCaipNamespace.Eip155;\n const normalizedAssetId = (isEvm ? assetId.toLowerCase() : assetId).replace(\n /:/gu,\n '/',\n );\n\n return `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/${normalizedAssetId}.png`;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Returns whether a balance row is a DeFi position carrying protocol metadata.\n *\n * @param balance - A balance row from the v6 API.\n * @returns True when the row is an `object: defi` row with protocol metadata.\n */\nfunction isDefiBalanceWithMetadata(\n balance: V6BalanceItem,\n): balance is DefiBalanceWithMetadata {\n return (\n balance.object === 'defi' &&\n balance.metadata !== undefined &&\n (balance.metadata as Partial<V6BalanceMetadata>).protocolId !== undefined\n );\n}\n\n/**\n * Returns the fiat market value for a v6 DeFi balance row.\n *\n * @param balance - A balance row from the v6 API.\n * @returns The fiat value, or `undefined` when price is missing or the\n * balance/price is invalid.\n */\nfunction getMarketValue(balance: V6BalanceItem): number | undefined {\n if (balance.price === undefined) {\n return undefined;\n }\n\n const normalizedBalance = Number.parseFloat(balance.balance);\n const price = Number.parseFloat(balance.price);\n\n if (!Number.isFinite(normalizedBalance) || !Number.isFinite(price)) {\n return undefined;\n }\n\n return normalizedBalance * price;\n}\n\n/**\n * Returns the sign used when rolling a position's market value into a protocol\n * group total. Liability types (currently `lending`) subtract; all others add.\n *\n * @param positionType - The position's protocol module type.\n * @returns `-1` for liabilities, `1` otherwise.\n */\nfunction getMarketValueSign(positionType: DeFiPositionType): 1 | -1 {\n return DEFI_POSITION_LIABILITY_TYPES.has(positionType) ? -1 : 1;\n}\n\n/**\n * Moves a priority symbol (ETH/WETH) to the front of the icon group, in place.\n *\n * @param iconGroup - The icon-group entries to reorder.\n */\nfunction orderIconGroup(iconGroup: DeFiPositionIconGroupItem[]): void {\n const priorityIndex = iconGroup.findIndex((item) =>\n SYMBOL_PRIORITY.includes(item.symbol),\n );\n\n if (priorityIndex > 0) {\n const [priorityIcon] = iconGroup.splice(priorityIndex, 1);\n iconGroup.unshift(priorityIcon);\n }\n}\n\n/**\n * Maps a DeFi balance row to a details-page underlying position.\n *\n * @param balance - A DeFi balance row with protocol metadata.\n * @returns The underlying position for the details page.\n */\nfunction toUnderlyingPosition(\n balance: DefiBalanceWithMetadata,\n): DeFiUnderlyingPosition {\n const assetId = balance.assetId as CaipAssetType;\n const { chainId } = parseCaipAssetType(assetId);\n const { positionType, poolAddress, groupId } = balance.metadata;\n\n return {\n assetId,\n chainId,\n symbol: balance.symbol,\n name: balance.name,\n balance: balance.balance,\n decimals: balance.decimals,\n marketValue: getMarketValue(balance),\n positionType,\n poolAddress,\n groupId,\n tokenImage: getDefiTokenImageUrl(assetId),\n };\n}\n\n/**\n * Builds a chain-reference-agnostic key (`namespace:address`) for matching the\n * CAIP-10 account IDs we request against the ones the v6 API echoes back.\n *\n * We request EVM balances with the all-chains reference (`eip155:0:<address>`),\n * but the response echoes a separate per-chain ID for every chain\n * (`eip155:1:<address>`, `eip155:137:<address>`, ...). Matching on the full\n * CAIP-10 string therefore fails, so we drop the reference and match on\n * namespace + address instead. EVM addresses are lowercased; other namespaces\n * keep their case.\n *\n * @param caipAccountId - A CAIP-10 account ID.\n * @returns The match key, or a case-normalized fallback if parsing fails.\n */\nfunction toAccountMatchKey(caipAccountId: string): string {\n try {\n const {\n chain: { namespace },\n address,\n } = parseCaipAccountId(caipAccountId as CaipAccountId);\n const normalizedAddress =\n namespace === KnownCaipNamespace.Eip155 ? address.toLowerCase() : address;\n return `${namespace}:${normalizedAddress}`;\n } catch {\n return caipAccountId.startsWith(`${KnownCaipNamespace.Eip155}:`)\n ? caipAccountId.toLowerCase()\n : caipAccountId;\n }\n}\n\n/**\n * Transforms a v6 multiaccount balances response into the stored DeFi state:\n * positions keyed by internal account ID, each mapping to a flat list of\n * protocol groups. Every group carries its own `chainId` (so the client can\n * filter without a nested chain map) plus both the DeFi-tab summary and the\n * details-page sections. When the request-account map is provided, every\n * requested account is included with an empty list so stale data is cleared\n * even when the flat response contains no rows for that account.\n *\n * When `internalAccountIdByCaip` is provided, response account IDs are matched\n * to internal MetaMask account IDs via namespace + address (ignoring chain\n * reference and EVM case). Unmatched accounts are skipped. When omitted, the\n * response account ID is used as-is (handy for unit tests).\n *\n * @param response - The v6 multiaccount balances response.\n * @param internalAccountIdByCaip - Optional map of request CAIP-10 account IDs\n * to internal MetaMask account IDs.\n * @returns DeFi positions keyed by internal account ID.\n */\nexport function groupDeFiPositionsV6(\n response: V6BalancesResponse,\n internalAccountIdByCaip?: Map<string, string>,\n): DeFiPositionsByAccount {\n const internalAccountIdByMatchKey = internalAccountIdByCaip\n ? new Map(\n [...internalAccountIdByCaip].map(([caipAccountId, internalId]) => [\n toAccountMatchKey(caipAccountId),\n internalId,\n ]),\n )\n : undefined;\n\n // Accumulate groups per resolved internal account ID. The v6 response rows\n // carry per-chain account IDs (e.g. `eip155:1:<addr>`,\n // `eip155:137:<addr>`), and several can resolve to the same internal account\n // ID, so merge across all of them.\n const groupsByAccountKey = new Map<\n string,\n Map<string, DeFiProtocolPositionGroup>\n >(\n internalAccountIdByCaip\n ? [...new Set(internalAccountIdByCaip.values())].map((accountId) => [\n accountId,\n new Map<string, DeFiProtocolPositionGroup>(),\n ])\n : [],\n );\n\n for (const balance of response.balances) {\n const accountId = internalAccountIdByMatchKey\n ? internalAccountIdByMatchKey.get(toAccountMatchKey(balance.accountId))\n : balance.accountId;\n if (accountId === undefined) {\n continue;\n }\n\n let groupsByKey = groupsByAccountKey.get(accountId);\n if (!groupsByKey) {\n groupsByKey = new Map<string, DeFiProtocolPositionGroup>();\n groupsByAccountKey.set(accountId, groupsByKey);\n }\n\n if (!isDefiBalanceWithMetadata(balance)) {\n continue;\n }\n\n const position = toUnderlyingPosition(balance);\n const { protocolId, productName, protocolIconUrl } = balance.metadata;\n const groupKey = `${position.chainId}#${protocolId}`;\n\n let group = groupsByKey.get(groupKey);\n if (!group) {\n group = {\n protocolId,\n productName,\n // Upstream may omit the icon; keep a string so clients do not need\n // optional handling on the stored group shape.\n protocolIconUrl: protocolIconUrl ?? '',\n chainId: position.chainId,\n marketValue: 0,\n iconGroup: [],\n sections: [],\n };\n groupsByKey.set(groupKey, group);\n }\n\n if (position.marketValue !== undefined) {\n group.marketValue +=\n position.marketValue * getMarketValueSign(position.positionType);\n }\n\n if (!group.iconGroup.some((item) => item.symbol === position.symbol)) {\n group.iconGroup.push({\n symbol: position.symbol,\n avatarValue: position.tokenImage,\n });\n }\n\n // Sections are keyed by productName; distinct groupIds under the same\n // product remain available on each underlying position.\n let section = group.sections.find(\n (item) => item.productName === productName,\n );\n if (!section) {\n section = { productName, positions: [] };\n group.sections.push(section);\n }\n section.positions.push(position);\n }\n\n const result: DeFiPositionsByAccount = {};\n for (const [accountId, groupsByKey] of groupsByAccountKey) {\n const groups = [...groupsByKey.values()];\n for (const group of groups) {\n orderIconGroup(group.iconGroup);\n }\n result[accountId] = groups;\n }\n\n return result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"group-defi-positions-v6.cjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/group-defi-positions-v6.ts"],"names":[],"mappings":"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","sourcesContent":["import { V6_DEFI_POSITION_TYPES } from '@metamask/core-backend';\nimport type {\n V6BalanceItem,\n V6BalanceMetadata,\n V6BalancesResponse,\n V6DeFiPositionType,\n} from '@metamask/core-backend';\nimport type {\n CaipAccountId,\n CaipAssetType,\n CaipChainId,\n} from '@metamask/utils';\nimport {\n KnownCaipNamespace,\n parseCaipAccountId,\n parseCaipAssetType,\n parseCaipChainId,\n} from '@metamask/utils';\n\n/** Static.cx host used to build CAIP-19 token icon URLs for DeFi positions. */\nconst STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io';\n\n/**\n * Possible `positionType` values from Accounts API v6 DeFi metadata.\n * Re-export of {@link V6_DEFI_POSITION_TYPES} from `@metamask/core-backend`.\n */\nexport const DEFI_POSITION_TYPES = V6_DEFI_POSITION_TYPES;\n\n/**\n * The specific module or functionality within a DeFi protocol where a position\n * is held. Alias of {@link V6DeFiPositionType} from `@metamask/core-backend`.\n */\nexport type DeFiPositionType = V6DeFiPositionType;\n\n/**\n * Position types whose fiat value is a liability and is subtracted from the\n * protocol group's aggregated `marketValue`.\n */\nexport const DEFI_POSITION_LIABILITY_TYPES: ReadonlySet<DeFiPositionType> =\n new Set<DeFiPositionType>(['loan']);\n\n/**\n * An icon-group entry shown next to a protocol in the DeFi tab list.\n */\nexport type DeFiPositionIconGroupItem = {\n /** Token icon URL, when one can be built for the asset. */\n avatarValue?: string;\n symbol: string;\n};\n\n/**\n * A single underlying position row shown on the DeFi details page.\n */\nexport type DeFiUnderlyingPosition = {\n assetId: CaipAssetType;\n chainId: CaipChainId;\n symbol: string;\n name: string;\n decimals: number;\n /** Raw balance string as returned by the API. */\n balance: string;\n /** Fiat market value in the requested currency, when a price is available. */\n marketValue?: number;\n /** Position type from protocol metadata. */\n positionType: DeFiPositionType;\n /** Address of the pool this position belongs to. */\n poolAddress: string;\n /**\n * Upstream grouping id from the API. Rows that share a `productName` can\n * still carry distinct `groupId`s (e.g. multiple Pendle YT markets).\n */\n groupId: string;\n /** Token icon URL, when one can be built for the asset. */\n tokenImage?: string;\n};\n\n/**\n * A section of the details page, grouping positions that share the same\n * API `productName`. A single `protocolId` can have multiple products\n * (different pools/markets under one protocol), so a group may contain\n * several sections.\n */\nexport type DeFiPositionDetailsSection = {\n /** Section label from the API (`metadata.productName`). */\n productName: string;\n positions: DeFiUnderlyingPosition[];\n};\n\n/**\n * One row in the DeFi tab list (a protocol on a given chain), with the details\n * needed to render the details page embedded directly inside it.\n */\nexport type DeFiProtocolPositionGroup = {\n protocolId: string;\n /**\n * Product name from the first position seen for this protocol. Prefer\n * `protocolId` for the list-row title; use section `productName`s for\n * per-product detail headings.\n */\n productName: string;\n protocolIconUrl: string;\n chainId: CaipChainId;\n /**\n * Aggregated fiat market value across all positions in the group.\n * `lending` positions are subtracted; all other types are added.\n */\n marketValue: number;\n /** Icon-group entries for the list row. */\n iconGroup: DeFiPositionIconGroupItem[];\n /**\n * Detail sections consumed by the details page, one per distinct API\n * `productName` under this `protocolId`.\n */\n sections: DeFiPositionDetailsSection[];\n};\n\n/**\n * DeFi positions for every queried account, keyed by the internal MetaMask\n * account ID (`InternalAccount.id` UUID), the same key AssetsController uses.\n * Each account maps to a flat list of protocol groups; filter by each group's\n * `chainId` rather than digging through a nested chain map.\n */\nexport type DeFiPositionsByAccount = {\n [accountId: string]: DeFiProtocolPositionGroup[];\n};\n\n// Prefer ETH/WETH first in the list-row icon stack when a protocol has multiple\n// underlyings. Display-only.\nconst SYMBOL_PRIORITY = ['ETH', 'WETH'];\n\ntype DefiBalanceWithMetadata = V6BalanceItem & { metadata: V6BalanceMetadata };\n\n/**\n * Builds a static token icon URL for a CAIP asset ID.\n *\n * @param assetId - The CAIP-19 asset ID.\n * @returns The token icon URL, or `undefined` when it cannot be built.\n */\nfunction getDefiTokenImageUrl(assetId: CaipAssetType): string | undefined {\n try {\n const { chainId } = parseCaipAssetType(assetId);\n const { namespace } = parseCaipChainId(chainId);\n const isEvm = namespace === KnownCaipNamespace.Eip155;\n const normalizedAssetId = (isEvm ? assetId.toLowerCase() : assetId).replace(\n /:/gu,\n '/',\n );\n\n return `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/${normalizedAssetId}.png`;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Returns whether a balance row is a DeFi position carrying protocol metadata.\n *\n * @param balance - A balance row from the v6 API.\n * @returns True when the row is a `category: defi` row with protocol metadata.\n */\nfunction isDefiBalanceWithMetadata(\n balance: V6BalanceItem,\n): balance is DefiBalanceWithMetadata {\n return (\n balance.category === 'defi' &&\n balance.metadata !== undefined &&\n (balance.metadata as Partial<V6BalanceMetadata>).protocolId !== undefined\n );\n}\n\n/**\n * Returns the fiat market value for a v6 DeFi balance row.\n *\n * @param balance - A balance row from the v6 API.\n * @returns The fiat value, or `undefined` when price is missing or the\n * balance/price is invalid.\n */\nfunction getMarketValue(balance: V6BalanceItem): number | undefined {\n if (balance.price === undefined) {\n return undefined;\n }\n\n const normalizedBalance = Number.parseFloat(balance.balance);\n const price = Number.parseFloat(balance.price);\n\n if (!Number.isFinite(normalizedBalance) || !Number.isFinite(price)) {\n return undefined;\n }\n\n return normalizedBalance * price;\n}\n\n/**\n * Returns the sign used when rolling a position's market value into a protocol\n * group total. Liability types (currently `lending`) subtract; all others add.\n *\n * @param positionType - The position's protocol module type.\n * @returns `-1` for liabilities, `1` otherwise.\n */\nfunction getMarketValueSign(positionType: DeFiPositionType): 1 | -1 {\n return DEFI_POSITION_LIABILITY_TYPES.has(positionType) ? -1 : 1;\n}\n\n/**\n * Moves a priority symbol (ETH/WETH) to the front of the icon group, in place.\n *\n * @param iconGroup - The icon-group entries to reorder.\n */\nfunction orderIconGroup(iconGroup: DeFiPositionIconGroupItem[]): void {\n const priorityIndex = iconGroup.findIndex((item) =>\n SYMBOL_PRIORITY.includes(item.symbol),\n );\n\n if (priorityIndex > 0) {\n const [priorityIcon] = iconGroup.splice(priorityIndex, 1);\n iconGroup.unshift(priorityIcon);\n }\n}\n\n/**\n * Maps a DeFi balance row to a details-page underlying position.\n *\n * @param balance - A DeFi balance row with protocol metadata.\n * @returns The underlying position for the details page.\n */\nfunction toUnderlyingPosition(\n balance: DefiBalanceWithMetadata,\n): DeFiUnderlyingPosition {\n const assetId = balance.assetId as CaipAssetType;\n const { chainId } = parseCaipAssetType(assetId);\n const { positionType, poolAddress, groupId } = balance.metadata;\n\n return {\n assetId,\n chainId,\n symbol: balance.symbol,\n name: balance.name,\n balance: balance.balance,\n decimals: balance.decimals,\n marketValue: getMarketValue(balance),\n positionType,\n poolAddress,\n groupId,\n tokenImage: getDefiTokenImageUrl(assetId),\n };\n}\n\n/**\n * Builds a chain-reference-agnostic key (`namespace:address`) for matching the\n * CAIP-10 account IDs we request against the ones the v6 API echoes back.\n *\n * We request EVM balances with the all-chains reference (`eip155:0:<address>`),\n * but the response echoes a separate per-chain ID for every chain\n * (`eip155:1:<address>`, `eip155:137:<address>`, ...). Matching on the full\n * CAIP-10 string therefore fails, so we drop the reference and match on\n * namespace + address instead. EVM addresses are lowercased; other namespaces\n * keep their case.\n *\n * @param caipAccountId - A CAIP-10 account ID.\n * @returns The match key, or a case-normalized fallback if parsing fails.\n */\nfunction toAccountMatchKey(caipAccountId: string): string {\n try {\n const {\n chain: { namespace },\n address,\n } = parseCaipAccountId(caipAccountId as CaipAccountId);\n const normalizedAddress =\n namespace === KnownCaipNamespace.Eip155 ? address.toLowerCase() : address;\n return `${namespace}:${normalizedAddress}`;\n } catch {\n return caipAccountId.startsWith(`${KnownCaipNamespace.Eip155}:`)\n ? caipAccountId.toLowerCase()\n : caipAccountId;\n }\n}\n\n/**\n * Transforms a v6 multiaccount balances response into the stored DeFi state:\n * positions keyed by internal account ID, each mapping to a flat list of\n * protocol groups. Every group carries its own `chainId` (so the client can\n * filter without a nested chain map) plus both the DeFi-tab summary and the\n * details-page sections. Accounts present in the response but with no DeFi\n * positions are included with an empty list so stale data is cleared.\n *\n * When `internalAccountIdByCaip` is provided, response account IDs are matched\n * to internal MetaMask account IDs via namespace + address (ignoring chain\n * reference and EVM case). Unmatched accounts are skipped. When omitted, the\n * response account ID is used as-is (handy for unit tests).\n *\n * @param response - The v6 multiaccount balances response.\n * @param internalAccountIdByCaip - Optional map of request CAIP-10 account IDs\n * to internal MetaMask account IDs.\n * @returns DeFi positions keyed by internal account ID.\n */\nexport function groupDeFiPositionsV6(\n response: V6BalancesResponse,\n internalAccountIdByCaip?: Map<string, string>,\n): DeFiPositionsByAccount {\n const internalAccountIdByMatchKey = internalAccountIdByCaip\n ? new Map(\n [...internalAccountIdByCaip].map(([caipAccountId, internalId]) => [\n toAccountMatchKey(caipAccountId),\n internalId,\n ]),\n )\n : undefined;\n\n // Accumulate groups per resolved internal account ID. The v6 response returns\n // a separate entry per chain (e.g. `eip155:1:<addr>`, `eip155:137:<addr>`),\n // and several of them can resolve to the same internal account ID, so we must\n // merge across all of them rather than overwrite per response account.\n const groupsByAccountKey = new Map<\n string,\n Map<string, DeFiProtocolPositionGroup>\n >();\n\n for (const account of response.accounts) {\n const accountId = internalAccountIdByMatchKey\n ? internalAccountIdByMatchKey.get(toAccountMatchKey(account.accountId))\n : account.accountId;\n if (accountId === undefined) {\n continue;\n }\n\n // Seed every queried account so accounts that no longer hold positions\n // overwrite (clear) any previously stored data.\n let groupsByKey = groupsByAccountKey.get(accountId);\n if (!groupsByKey) {\n groupsByKey = new Map<string, DeFiProtocolPositionGroup>();\n groupsByAccountKey.set(accountId, groupsByKey);\n }\n\n for (const balance of account.balances) {\n if (!isDefiBalanceWithMetadata(balance)) {\n continue;\n }\n\n const position = toUnderlyingPosition(balance);\n const { protocolId, productName, protocolIconUrl } = balance.metadata;\n const groupKey = `${position.chainId}#${protocolId}`;\n\n let group = groupsByKey.get(groupKey);\n if (!group) {\n group = {\n protocolId,\n productName,\n protocolIconUrl,\n chainId: position.chainId,\n marketValue: 0,\n iconGroup: [],\n sections: [],\n };\n groupsByKey.set(groupKey, group);\n }\n\n if (position.marketValue !== undefined) {\n group.marketValue +=\n position.marketValue * getMarketValueSign(position.positionType);\n }\n\n if (!group.iconGroup.some((item) => item.symbol === position.symbol)) {\n group.iconGroup.push({\n symbol: position.symbol,\n avatarValue: position.tokenImage,\n });\n }\n\n // Sections are keyed by productName; distinct groupIds under the same\n // product remain available on each underlying position.\n let section = group.sections.find(\n (item) => item.productName === productName,\n );\n if (!section) {\n section = { productName, positions: [] };\n group.sections.push(section);\n }\n section.positions.push(position);\n }\n }\n\n const result: DeFiPositionsByAccount = {};\n for (const [accountId, groupsByKey] of groupsByAccountKey) {\n const groups = [...groupsByKey.values()];\n for (const group of groups) {\n orderIconGroup(group.iconGroup);\n }\n result[accountId] = groups;\n }\n\n return result;\n}\n"]}
|
|
@@ -100,9 +100,8 @@ export type DeFiPositionsByAccount = {
|
|
|
100
100
|
* positions keyed by internal account ID, each mapping to a flat list of
|
|
101
101
|
* protocol groups. Every group carries its own `chainId` (so the client can
|
|
102
102
|
* filter without a nested chain map) plus both the DeFi-tab summary and the
|
|
103
|
-
* details-page sections.
|
|
104
|
-
*
|
|
105
|
-
* even when the flat response contains no rows for that account.
|
|
103
|
+
* details-page sections. Accounts present in the response but with no DeFi
|
|
104
|
+
* positions are included with an empty list so stale data is cleared.
|
|
106
105
|
*
|
|
107
106
|
* When `internalAccountIdByCaip` is provided, response account IDs are matched
|
|
108
107
|
* to internal MetaMask account IDs via namespace + address (ignoring chain
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-defi-positions-v6.d.cts","sourceRoot":"","sources":["../../src/DeFiPositionsController/group-defi-positions-v6.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,kBAAkB,EAClB,kBAAkB,EACnB,+BAA+B;AAChC,OAAO,KAAK,EAEV,aAAa,EACb,WAAW,EACZ,wBAAwB;AAWzB;;;GAGG;AACH,eAAO,MAAM,mBAAmB,oFAAyB,CAAC;AAE1D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAElD;;;GAGG;AACH,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,gBAAgB,CACnC,CAAC;AAEtC;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,YAAY,EAAE,gBAAgB,CAAC;IAC/B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,sBAAsB,EAAE,CAAC;CACrC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,SAAS,EAAE,yBAAyB,EAAE,CAAC;IACvC;;;OAGG;IACH,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,CAAC,SAAS,EAAE,MAAM,GAAG,yBAAyB,EAAE,CAAC;CAClD,CAAC;AAyJF
|
|
1
|
+
{"version":3,"file":"group-defi-positions-v6.d.cts","sourceRoot":"","sources":["../../src/DeFiPositionsController/group-defi-positions-v6.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,kBAAkB,EAClB,kBAAkB,EACnB,+BAA+B;AAChC,OAAO,KAAK,EAEV,aAAa,EACb,WAAW,EACZ,wBAAwB;AAWzB;;;GAGG;AACH,eAAO,MAAM,mBAAmB,oFAAyB,CAAC;AAE1D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAElD;;;GAGG;AACH,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,gBAAgB,CACnC,CAAC;AAEtC;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,YAAY,EAAE,gBAAgB,CAAC;IAC/B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,sBAAsB,EAAE,CAAC;CACrC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,SAAS,EAAE,yBAAyB,EAAE,CAAC;IACvC;;;OAGG;IACH,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,CAAC,SAAS,EAAE,MAAM,GAAG,yBAAyB,EAAE,CAAC;CAClD,CAAC;AAyJF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,kBAAkB,EAC5B,uBAAuB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5C,sBAAsB,CA6FxB"}
|
|
@@ -100,9 +100,8 @@ export type DeFiPositionsByAccount = {
|
|
|
100
100
|
* positions keyed by internal account ID, each mapping to a flat list of
|
|
101
101
|
* protocol groups. Every group carries its own `chainId` (so the client can
|
|
102
102
|
* filter without a nested chain map) plus both the DeFi-tab summary and the
|
|
103
|
-
* details-page sections.
|
|
104
|
-
*
|
|
105
|
-
* even when the flat response contains no rows for that account.
|
|
103
|
+
* details-page sections. Accounts present in the response but with no DeFi
|
|
104
|
+
* positions are included with an empty list so stale data is cleared.
|
|
106
105
|
*
|
|
107
106
|
* When `internalAccountIdByCaip` is provided, response account IDs are matched
|
|
108
107
|
* to internal MetaMask account IDs via namespace + address (ignoring chain
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-defi-positions-v6.d.mts","sourceRoot":"","sources":["../../src/DeFiPositionsController/group-defi-positions-v6.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,kBAAkB,EAClB,kBAAkB,EACnB,+BAA+B;AAChC,OAAO,KAAK,EAEV,aAAa,EACb,WAAW,EACZ,wBAAwB;AAWzB;;;GAGG;AACH,eAAO,MAAM,mBAAmB,oFAAyB,CAAC;AAE1D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAElD;;;GAGG;AACH,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,gBAAgB,CACnC,CAAC;AAEtC;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,YAAY,EAAE,gBAAgB,CAAC;IAC/B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,sBAAsB,EAAE,CAAC;CACrC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,SAAS,EAAE,yBAAyB,EAAE,CAAC;IACvC;;;OAGG;IACH,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,CAAC,SAAS,EAAE,MAAM,GAAG,yBAAyB,EAAE,CAAC;CAClD,CAAC;AAyJF
|
|
1
|
+
{"version":3,"file":"group-defi-positions-v6.d.mts","sourceRoot":"","sources":["../../src/DeFiPositionsController/group-defi-positions-v6.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAGV,kBAAkB,EAClB,kBAAkB,EACnB,+BAA+B;AAChC,OAAO,KAAK,EAEV,aAAa,EACb,WAAW,EACZ,wBAAwB;AAWzB;;;GAGG;AACH,eAAO,MAAM,mBAAmB,oFAAyB,CAAC;AAE1D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAElD;;;GAGG;AACH,eAAO,MAAM,6BAA6B,EAAE,WAAW,CAAC,gBAAgB,CACnC,CAAC;AAEtC;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4CAA4C;IAC5C,YAAY,EAAE,gBAAgB,CAAC;IAC/B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,sBAAsB,EAAE,CAAC;CACrC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,SAAS,EAAE,yBAAyB,EAAE,CAAC;IACvC;;;OAGG;IACH,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,CAAC,SAAS,EAAE,MAAM,GAAG,yBAAyB,EAAE,CAAC;CAClD,CAAC;AAyJF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,kBAAkB,EAC5B,uBAAuB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5C,sBAAsB,CA6FxB"}
|
|
@@ -37,10 +37,10 @@ function getDefiTokenImageUrl(assetId) {
|
|
|
37
37
|
* Returns whether a balance row is a DeFi position carrying protocol metadata.
|
|
38
38
|
*
|
|
39
39
|
* @param balance - A balance row from the v6 API.
|
|
40
|
-
* @returns True when the row is
|
|
40
|
+
* @returns True when the row is a `category: defi` row with protocol metadata.
|
|
41
41
|
*/
|
|
42
42
|
function isDefiBalanceWithMetadata(balance) {
|
|
43
|
-
return (balance.
|
|
43
|
+
return (balance.category === 'defi' &&
|
|
44
44
|
balance.metadata !== undefined &&
|
|
45
45
|
balance.metadata.protocolId !== undefined);
|
|
46
46
|
}
|
|
@@ -139,9 +139,8 @@ function toAccountMatchKey(caipAccountId) {
|
|
|
139
139
|
* positions keyed by internal account ID, each mapping to a flat list of
|
|
140
140
|
* protocol groups. Every group carries its own `chainId` (so the client can
|
|
141
141
|
* filter without a nested chain map) plus both the DeFi-tab summary and the
|
|
142
|
-
* details-page sections.
|
|
143
|
-
*
|
|
144
|
-
* even when the flat response contains no rows for that account.
|
|
142
|
+
* details-page sections. Accounts present in the response but with no DeFi
|
|
143
|
+
* positions are included with an empty list so stale data is cleared.
|
|
145
144
|
*
|
|
146
145
|
* When `internalAccountIdByCaip` is provided, response account IDs are matched
|
|
147
146
|
* to internal MetaMask account IDs via namespace + address (ignoring chain
|
|
@@ -160,67 +159,64 @@ export function groupDeFiPositionsV6(response, internalAccountIdByCaip) {
|
|
|
160
159
|
internalId,
|
|
161
160
|
]))
|
|
162
161
|
: undefined;
|
|
163
|
-
// Accumulate groups per resolved internal account ID. The v6 response
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
const groupsByAccountKey = new Map(
|
|
168
|
-
|
|
169
|
-
accountId,
|
|
170
|
-
new Map(),
|
|
171
|
-
])
|
|
172
|
-
: []);
|
|
173
|
-
for (const balance of response.balances) {
|
|
162
|
+
// Accumulate groups per resolved internal account ID. The v6 response returns
|
|
163
|
+
// a separate entry per chain (e.g. `eip155:1:<addr>`, `eip155:137:<addr>`),
|
|
164
|
+
// and several of them can resolve to the same internal account ID, so we must
|
|
165
|
+
// merge across all of them rather than overwrite per response account.
|
|
166
|
+
const groupsByAccountKey = new Map();
|
|
167
|
+
for (const account of response.accounts) {
|
|
174
168
|
const accountId = internalAccountIdByMatchKey
|
|
175
|
-
? internalAccountIdByMatchKey.get(toAccountMatchKey(
|
|
176
|
-
:
|
|
169
|
+
? internalAccountIdByMatchKey.get(toAccountMatchKey(account.accountId))
|
|
170
|
+
: account.accountId;
|
|
177
171
|
if (accountId === undefined) {
|
|
178
172
|
continue;
|
|
179
173
|
}
|
|
174
|
+
// Seed every queried account so accounts that no longer hold positions
|
|
175
|
+
// overwrite (clear) any previously stored data.
|
|
180
176
|
let groupsByKey = groupsByAccountKey.get(accountId);
|
|
181
177
|
if (!groupsByKey) {
|
|
182
178
|
groupsByKey = new Map();
|
|
183
179
|
groupsByAccountKey.set(accountId, groupsByKey);
|
|
184
180
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
group
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
181
|
+
for (const balance of account.balances) {
|
|
182
|
+
if (!isDefiBalanceWithMetadata(balance)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const position = toUnderlyingPosition(balance);
|
|
186
|
+
const { protocolId, productName, protocolIconUrl } = balance.metadata;
|
|
187
|
+
const groupKey = `${position.chainId}#${protocolId}`;
|
|
188
|
+
let group = groupsByKey.get(groupKey);
|
|
189
|
+
if (!group) {
|
|
190
|
+
group = {
|
|
191
|
+
protocolId,
|
|
192
|
+
productName,
|
|
193
|
+
protocolIconUrl,
|
|
194
|
+
chainId: position.chainId,
|
|
195
|
+
marketValue: 0,
|
|
196
|
+
iconGroup: [],
|
|
197
|
+
sections: [],
|
|
198
|
+
};
|
|
199
|
+
groupsByKey.set(groupKey, group);
|
|
200
|
+
}
|
|
201
|
+
if (position.marketValue !== undefined) {
|
|
202
|
+
group.marketValue +=
|
|
203
|
+
position.marketValue * getMarketValueSign(position.positionType);
|
|
204
|
+
}
|
|
205
|
+
if (!group.iconGroup.some((item) => item.symbol === position.symbol)) {
|
|
206
|
+
group.iconGroup.push({
|
|
207
|
+
symbol: position.symbol,
|
|
208
|
+
avatarValue: position.tokenImage,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Sections are keyed by productName; distinct groupIds under the same
|
|
212
|
+
// product remain available on each underlying position.
|
|
213
|
+
let section = group.sections.find((item) => item.productName === productName);
|
|
214
|
+
if (!section) {
|
|
215
|
+
section = { productName, positions: [] };
|
|
216
|
+
group.sections.push(section);
|
|
217
|
+
}
|
|
218
|
+
section.positions.push(position);
|
|
222
219
|
}
|
|
223
|
-
section.positions.push(position);
|
|
224
220
|
}
|
|
225
221
|
const result = {};
|
|
226
222
|
for (const [accountId, groupsByKey] of groupsByAccountKey) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"group-defi-positions-v6.mjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/group-defi-positions-v6.ts"],"names":[],"mappings":"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","sourcesContent":["import { V6_DEFI_POSITION_TYPES } from '@metamask/core-backend';\nimport type {\n V6BalanceItem,\n V6BalanceMetadata,\n V6BalancesResponse,\n V6DeFiPositionType,\n} from '@metamask/core-backend';\nimport type {\n CaipAccountId,\n CaipAssetType,\n CaipChainId,\n} from '@metamask/utils';\nimport {\n KnownCaipNamespace,\n parseCaipAccountId,\n parseCaipAssetType,\n parseCaipChainId,\n} from '@metamask/utils';\n\n/** Static.cx host used to build CAIP-19 token icon URLs for DeFi positions. */\nconst STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io';\n\n/**\n * Possible `positionType` values from Accounts API v6 DeFi metadata.\n * Re-export of {@link V6_DEFI_POSITION_TYPES} from `@metamask/core-backend`.\n */\nexport const DEFI_POSITION_TYPES = V6_DEFI_POSITION_TYPES;\n\n/**\n * The specific module or functionality within a DeFi protocol where a position\n * is held. Alias of {@link V6DeFiPositionType} from `@metamask/core-backend`.\n */\nexport type DeFiPositionType = V6DeFiPositionType;\n\n/**\n * Position types whose fiat value is a liability and is subtracted from the\n * protocol group's aggregated `marketValue`.\n */\nexport const DEFI_POSITION_LIABILITY_TYPES: ReadonlySet<DeFiPositionType> =\n new Set<DeFiPositionType>(['loan']);\n\n/**\n * An icon-group entry shown next to a protocol in the DeFi tab list.\n */\nexport type DeFiPositionIconGroupItem = {\n /** Token icon URL, when one can be built for the asset. */\n avatarValue?: string;\n symbol: string;\n};\n\n/**\n * A single underlying position row shown on the DeFi details page.\n */\nexport type DeFiUnderlyingPosition = {\n assetId: CaipAssetType;\n chainId: CaipChainId;\n symbol: string;\n name: string;\n decimals: number;\n /** Raw balance string as returned by the API. */\n balance: string;\n /** Fiat market value in the requested currency, when a price is available. */\n marketValue?: number;\n /** Position type from protocol metadata. */\n positionType: DeFiPositionType;\n /** Address of the pool this position belongs to. */\n poolAddress: string;\n /**\n * Upstream grouping id from the API. Rows that share a `productName` can\n * still carry distinct `groupId`s (e.g. multiple Pendle YT markets).\n */\n groupId: string;\n /** Token icon URL, when one can be built for the asset. */\n tokenImage?: string;\n};\n\n/**\n * A section of the details page, grouping positions that share the same\n * API `productName`. A single `protocolId` can have multiple products\n * (different pools/markets under one protocol), so a group may contain\n * several sections.\n */\nexport type DeFiPositionDetailsSection = {\n /** Section label from the API (`metadata.productName`). */\n productName: string;\n positions: DeFiUnderlyingPosition[];\n};\n\n/**\n * One row in the DeFi tab list (a protocol on a given chain), with the details\n * needed to render the details page embedded directly inside it.\n */\nexport type DeFiProtocolPositionGroup = {\n protocolId: string;\n /**\n * Product name from the first position seen for this protocol. Prefer\n * `protocolId` for the list-row title; use section `productName`s for\n * per-product detail headings.\n */\n productName: string;\n protocolIconUrl: string;\n chainId: CaipChainId;\n /**\n * Aggregated fiat market value across all positions in the group.\n * `lending` positions are subtracted; all other types are added.\n */\n marketValue: number;\n /** Icon-group entries for the list row. */\n iconGroup: DeFiPositionIconGroupItem[];\n /**\n * Detail sections consumed by the details page, one per distinct API\n * `productName` under this `protocolId`.\n */\n sections: DeFiPositionDetailsSection[];\n};\n\n/**\n * DeFi positions for every queried account, keyed by the internal MetaMask\n * account ID (`InternalAccount.id` UUID), the same key AssetsController uses.\n * Each account maps to a flat list of protocol groups; filter by each group's\n * `chainId` rather than digging through a nested chain map.\n */\nexport type DeFiPositionsByAccount = {\n [accountId: string]: DeFiProtocolPositionGroup[];\n};\n\n// Prefer ETH/WETH first in the list-row icon stack when a protocol has multiple\n// underlyings. Display-only.\nconst SYMBOL_PRIORITY = ['ETH', 'WETH'];\n\ntype DefiBalanceWithMetadata = V6BalanceItem & { metadata: V6BalanceMetadata };\n\n/**\n * Builds a static token icon URL for a CAIP asset ID.\n *\n * @param assetId - The CAIP-19 asset ID.\n * @returns The token icon URL, or `undefined` when it cannot be built.\n */\nfunction getDefiTokenImageUrl(assetId: CaipAssetType): string | undefined {\n try {\n const { chainId } = parseCaipAssetType(assetId);\n const { namespace } = parseCaipChainId(chainId);\n const isEvm = namespace === KnownCaipNamespace.Eip155;\n const normalizedAssetId = (isEvm ? assetId.toLowerCase() : assetId).replace(\n /:/gu,\n '/',\n );\n\n return `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/${normalizedAssetId}.png`;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Returns whether a balance row is a DeFi position carrying protocol metadata.\n *\n * @param balance - A balance row from the v6 API.\n * @returns True when the row is an `object: defi` row with protocol metadata.\n */\nfunction isDefiBalanceWithMetadata(\n balance: V6BalanceItem,\n): balance is DefiBalanceWithMetadata {\n return (\n balance.object === 'defi' &&\n balance.metadata !== undefined &&\n (balance.metadata as Partial<V6BalanceMetadata>).protocolId !== undefined\n );\n}\n\n/**\n * Returns the fiat market value for a v6 DeFi balance row.\n *\n * @param balance - A balance row from the v6 API.\n * @returns The fiat value, or `undefined` when price is missing or the\n * balance/price is invalid.\n */\nfunction getMarketValue(balance: V6BalanceItem): number | undefined {\n if (balance.price === undefined) {\n return undefined;\n }\n\n const normalizedBalance = Number.parseFloat(balance.balance);\n const price = Number.parseFloat(balance.price);\n\n if (!Number.isFinite(normalizedBalance) || !Number.isFinite(price)) {\n return undefined;\n }\n\n return normalizedBalance * price;\n}\n\n/**\n * Returns the sign used when rolling a position's market value into a protocol\n * group total. Liability types (currently `lending`) subtract; all others add.\n *\n * @param positionType - The position's protocol module type.\n * @returns `-1` for liabilities, `1` otherwise.\n */\nfunction getMarketValueSign(positionType: DeFiPositionType): 1 | -1 {\n return DEFI_POSITION_LIABILITY_TYPES.has(positionType) ? -1 : 1;\n}\n\n/**\n * Moves a priority symbol (ETH/WETH) to the front of the icon group, in place.\n *\n * @param iconGroup - The icon-group entries to reorder.\n */\nfunction orderIconGroup(iconGroup: DeFiPositionIconGroupItem[]): void {\n const priorityIndex = iconGroup.findIndex((item) =>\n SYMBOL_PRIORITY.includes(item.symbol),\n );\n\n if (priorityIndex > 0) {\n const [priorityIcon] = iconGroup.splice(priorityIndex, 1);\n iconGroup.unshift(priorityIcon);\n }\n}\n\n/**\n * Maps a DeFi balance row to a details-page underlying position.\n *\n * @param balance - A DeFi balance row with protocol metadata.\n * @returns The underlying position for the details page.\n */\nfunction toUnderlyingPosition(\n balance: DefiBalanceWithMetadata,\n): DeFiUnderlyingPosition {\n const assetId = balance.assetId as CaipAssetType;\n const { chainId } = parseCaipAssetType(assetId);\n const { positionType, poolAddress, groupId } = balance.metadata;\n\n return {\n assetId,\n chainId,\n symbol: balance.symbol,\n name: balance.name,\n balance: balance.balance,\n decimals: balance.decimals,\n marketValue: getMarketValue(balance),\n positionType,\n poolAddress,\n groupId,\n tokenImage: getDefiTokenImageUrl(assetId),\n };\n}\n\n/**\n * Builds a chain-reference-agnostic key (`namespace:address`) for matching the\n * CAIP-10 account IDs we request against the ones the v6 API echoes back.\n *\n * We request EVM balances with the all-chains reference (`eip155:0:<address>`),\n * but the response echoes a separate per-chain ID for every chain\n * (`eip155:1:<address>`, `eip155:137:<address>`, ...). Matching on the full\n * CAIP-10 string therefore fails, so we drop the reference and match on\n * namespace + address instead. EVM addresses are lowercased; other namespaces\n * keep their case.\n *\n * @param caipAccountId - A CAIP-10 account ID.\n * @returns The match key, or a case-normalized fallback if parsing fails.\n */\nfunction toAccountMatchKey(caipAccountId: string): string {\n try {\n const {\n chain: { namespace },\n address,\n } = parseCaipAccountId(caipAccountId as CaipAccountId);\n const normalizedAddress =\n namespace === KnownCaipNamespace.Eip155 ? address.toLowerCase() : address;\n return `${namespace}:${normalizedAddress}`;\n } catch {\n return caipAccountId.startsWith(`${KnownCaipNamespace.Eip155}:`)\n ? caipAccountId.toLowerCase()\n : caipAccountId;\n }\n}\n\n/**\n * Transforms a v6 multiaccount balances response into the stored DeFi state:\n * positions keyed by internal account ID, each mapping to a flat list of\n * protocol groups. Every group carries its own `chainId` (so the client can\n * filter without a nested chain map) plus both the DeFi-tab summary and the\n * details-page sections. When the request-account map is provided, every\n * requested account is included with an empty list so stale data is cleared\n * even when the flat response contains no rows for that account.\n *\n * When `internalAccountIdByCaip` is provided, response account IDs are matched\n * to internal MetaMask account IDs via namespace + address (ignoring chain\n * reference and EVM case). Unmatched accounts are skipped. When omitted, the\n * response account ID is used as-is (handy for unit tests).\n *\n * @param response - The v6 multiaccount balances response.\n * @param internalAccountIdByCaip - Optional map of request CAIP-10 account IDs\n * to internal MetaMask account IDs.\n * @returns DeFi positions keyed by internal account ID.\n */\nexport function groupDeFiPositionsV6(\n response: V6BalancesResponse,\n internalAccountIdByCaip?: Map<string, string>,\n): DeFiPositionsByAccount {\n const internalAccountIdByMatchKey = internalAccountIdByCaip\n ? new Map(\n [...internalAccountIdByCaip].map(([caipAccountId, internalId]) => [\n toAccountMatchKey(caipAccountId),\n internalId,\n ]),\n )\n : undefined;\n\n // Accumulate groups per resolved internal account ID. The v6 response rows\n // carry per-chain account IDs (e.g. `eip155:1:<addr>`,\n // `eip155:137:<addr>`), and several can resolve to the same internal account\n // ID, so merge across all of them.\n const groupsByAccountKey = new Map<\n string,\n Map<string, DeFiProtocolPositionGroup>\n >(\n internalAccountIdByCaip\n ? [...new Set(internalAccountIdByCaip.values())].map((accountId) => [\n accountId,\n new Map<string, DeFiProtocolPositionGroup>(),\n ])\n : [],\n );\n\n for (const balance of response.balances) {\n const accountId = internalAccountIdByMatchKey\n ? internalAccountIdByMatchKey.get(toAccountMatchKey(balance.accountId))\n : balance.accountId;\n if (accountId === undefined) {\n continue;\n }\n\n let groupsByKey = groupsByAccountKey.get(accountId);\n if (!groupsByKey) {\n groupsByKey = new Map<string, DeFiProtocolPositionGroup>();\n groupsByAccountKey.set(accountId, groupsByKey);\n }\n\n if (!isDefiBalanceWithMetadata(balance)) {\n continue;\n }\n\n const position = toUnderlyingPosition(balance);\n const { protocolId, productName, protocolIconUrl } = balance.metadata;\n const groupKey = `${position.chainId}#${protocolId}`;\n\n let group = groupsByKey.get(groupKey);\n if (!group) {\n group = {\n protocolId,\n productName,\n // Upstream may omit the icon; keep a string so clients do not need\n // optional handling on the stored group shape.\n protocolIconUrl: protocolIconUrl ?? '',\n chainId: position.chainId,\n marketValue: 0,\n iconGroup: [],\n sections: [],\n };\n groupsByKey.set(groupKey, group);\n }\n\n if (position.marketValue !== undefined) {\n group.marketValue +=\n position.marketValue * getMarketValueSign(position.positionType);\n }\n\n if (!group.iconGroup.some((item) => item.symbol === position.symbol)) {\n group.iconGroup.push({\n symbol: position.symbol,\n avatarValue: position.tokenImage,\n });\n }\n\n // Sections are keyed by productName; distinct groupIds under the same\n // product remain available on each underlying position.\n let section = group.sections.find(\n (item) => item.productName === productName,\n );\n if (!section) {\n section = { productName, positions: [] };\n group.sections.push(section);\n }\n section.positions.push(position);\n }\n\n const result: DeFiPositionsByAccount = {};\n for (const [accountId, groupsByKey] of groupsByAccountKey) {\n const groups = [...groupsByKey.values()];\n for (const group of groups) {\n orderIconGroup(group.iconGroup);\n }\n result[accountId] = groups;\n }\n\n return result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"group-defi-positions-v6.mjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/group-defi-positions-v6.ts"],"names":[],"mappings":"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","sourcesContent":["import { V6_DEFI_POSITION_TYPES } from '@metamask/core-backend';\nimport type {\n V6BalanceItem,\n V6BalanceMetadata,\n V6BalancesResponse,\n V6DeFiPositionType,\n} from '@metamask/core-backend';\nimport type {\n CaipAccountId,\n CaipAssetType,\n CaipChainId,\n} from '@metamask/utils';\nimport {\n KnownCaipNamespace,\n parseCaipAccountId,\n parseCaipAssetType,\n parseCaipChainId,\n} from '@metamask/utils';\n\n/** Static.cx host used to build CAIP-19 token icon URLs for DeFi positions. */\nconst STATIC_METAMASK_BASE_URL = 'https://static.cx.metamask.io';\n\n/**\n * Possible `positionType` values from Accounts API v6 DeFi metadata.\n * Re-export of {@link V6_DEFI_POSITION_TYPES} from `@metamask/core-backend`.\n */\nexport const DEFI_POSITION_TYPES = V6_DEFI_POSITION_TYPES;\n\n/**\n * The specific module or functionality within a DeFi protocol where a position\n * is held. Alias of {@link V6DeFiPositionType} from `@metamask/core-backend`.\n */\nexport type DeFiPositionType = V6DeFiPositionType;\n\n/**\n * Position types whose fiat value is a liability and is subtracted from the\n * protocol group's aggregated `marketValue`.\n */\nexport const DEFI_POSITION_LIABILITY_TYPES: ReadonlySet<DeFiPositionType> =\n new Set<DeFiPositionType>(['loan']);\n\n/**\n * An icon-group entry shown next to a protocol in the DeFi tab list.\n */\nexport type DeFiPositionIconGroupItem = {\n /** Token icon URL, when one can be built for the asset. */\n avatarValue?: string;\n symbol: string;\n};\n\n/**\n * A single underlying position row shown on the DeFi details page.\n */\nexport type DeFiUnderlyingPosition = {\n assetId: CaipAssetType;\n chainId: CaipChainId;\n symbol: string;\n name: string;\n decimals: number;\n /** Raw balance string as returned by the API. */\n balance: string;\n /** Fiat market value in the requested currency, when a price is available. */\n marketValue?: number;\n /** Position type from protocol metadata. */\n positionType: DeFiPositionType;\n /** Address of the pool this position belongs to. */\n poolAddress: string;\n /**\n * Upstream grouping id from the API. Rows that share a `productName` can\n * still carry distinct `groupId`s (e.g. multiple Pendle YT markets).\n */\n groupId: string;\n /** Token icon URL, when one can be built for the asset. */\n tokenImage?: string;\n};\n\n/**\n * A section of the details page, grouping positions that share the same\n * API `productName`. A single `protocolId` can have multiple products\n * (different pools/markets under one protocol), so a group may contain\n * several sections.\n */\nexport type DeFiPositionDetailsSection = {\n /** Section label from the API (`metadata.productName`). */\n productName: string;\n positions: DeFiUnderlyingPosition[];\n};\n\n/**\n * One row in the DeFi tab list (a protocol on a given chain), with the details\n * needed to render the details page embedded directly inside it.\n */\nexport type DeFiProtocolPositionGroup = {\n protocolId: string;\n /**\n * Product name from the first position seen for this protocol. Prefer\n * `protocolId` for the list-row title; use section `productName`s for\n * per-product detail headings.\n */\n productName: string;\n protocolIconUrl: string;\n chainId: CaipChainId;\n /**\n * Aggregated fiat market value across all positions in the group.\n * `lending` positions are subtracted; all other types are added.\n */\n marketValue: number;\n /** Icon-group entries for the list row. */\n iconGroup: DeFiPositionIconGroupItem[];\n /**\n * Detail sections consumed by the details page, one per distinct API\n * `productName` under this `protocolId`.\n */\n sections: DeFiPositionDetailsSection[];\n};\n\n/**\n * DeFi positions for every queried account, keyed by the internal MetaMask\n * account ID (`InternalAccount.id` UUID), the same key AssetsController uses.\n * Each account maps to a flat list of protocol groups; filter by each group's\n * `chainId` rather than digging through a nested chain map.\n */\nexport type DeFiPositionsByAccount = {\n [accountId: string]: DeFiProtocolPositionGroup[];\n};\n\n// Prefer ETH/WETH first in the list-row icon stack when a protocol has multiple\n// underlyings. Display-only.\nconst SYMBOL_PRIORITY = ['ETH', 'WETH'];\n\ntype DefiBalanceWithMetadata = V6BalanceItem & { metadata: V6BalanceMetadata };\n\n/**\n * Builds a static token icon URL for a CAIP asset ID.\n *\n * @param assetId - The CAIP-19 asset ID.\n * @returns The token icon URL, or `undefined` when it cannot be built.\n */\nfunction getDefiTokenImageUrl(assetId: CaipAssetType): string | undefined {\n try {\n const { chainId } = parseCaipAssetType(assetId);\n const { namespace } = parseCaipChainId(chainId);\n const isEvm = namespace === KnownCaipNamespace.Eip155;\n const normalizedAssetId = (isEvm ? assetId.toLowerCase() : assetId).replace(\n /:/gu,\n '/',\n );\n\n return `${STATIC_METAMASK_BASE_URL}/api/v2/tokenIcons/assets/${normalizedAssetId}.png`;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Returns whether a balance row is a DeFi position carrying protocol metadata.\n *\n * @param balance - A balance row from the v6 API.\n * @returns True when the row is a `category: defi` row with protocol metadata.\n */\nfunction isDefiBalanceWithMetadata(\n balance: V6BalanceItem,\n): balance is DefiBalanceWithMetadata {\n return (\n balance.category === 'defi' &&\n balance.metadata !== undefined &&\n (balance.metadata as Partial<V6BalanceMetadata>).protocolId !== undefined\n );\n}\n\n/**\n * Returns the fiat market value for a v6 DeFi balance row.\n *\n * @param balance - A balance row from the v6 API.\n * @returns The fiat value, or `undefined` when price is missing or the\n * balance/price is invalid.\n */\nfunction getMarketValue(balance: V6BalanceItem): number | undefined {\n if (balance.price === undefined) {\n return undefined;\n }\n\n const normalizedBalance = Number.parseFloat(balance.balance);\n const price = Number.parseFloat(balance.price);\n\n if (!Number.isFinite(normalizedBalance) || !Number.isFinite(price)) {\n return undefined;\n }\n\n return normalizedBalance * price;\n}\n\n/**\n * Returns the sign used when rolling a position's market value into a protocol\n * group total. Liability types (currently `lending`) subtract; all others add.\n *\n * @param positionType - The position's protocol module type.\n * @returns `-1` for liabilities, `1` otherwise.\n */\nfunction getMarketValueSign(positionType: DeFiPositionType): 1 | -1 {\n return DEFI_POSITION_LIABILITY_TYPES.has(positionType) ? -1 : 1;\n}\n\n/**\n * Moves a priority symbol (ETH/WETH) to the front of the icon group, in place.\n *\n * @param iconGroup - The icon-group entries to reorder.\n */\nfunction orderIconGroup(iconGroup: DeFiPositionIconGroupItem[]): void {\n const priorityIndex = iconGroup.findIndex((item) =>\n SYMBOL_PRIORITY.includes(item.symbol),\n );\n\n if (priorityIndex > 0) {\n const [priorityIcon] = iconGroup.splice(priorityIndex, 1);\n iconGroup.unshift(priorityIcon);\n }\n}\n\n/**\n * Maps a DeFi balance row to a details-page underlying position.\n *\n * @param balance - A DeFi balance row with protocol metadata.\n * @returns The underlying position for the details page.\n */\nfunction toUnderlyingPosition(\n balance: DefiBalanceWithMetadata,\n): DeFiUnderlyingPosition {\n const assetId = balance.assetId as CaipAssetType;\n const { chainId } = parseCaipAssetType(assetId);\n const { positionType, poolAddress, groupId } = balance.metadata;\n\n return {\n assetId,\n chainId,\n symbol: balance.symbol,\n name: balance.name,\n balance: balance.balance,\n decimals: balance.decimals,\n marketValue: getMarketValue(balance),\n positionType,\n poolAddress,\n groupId,\n tokenImage: getDefiTokenImageUrl(assetId),\n };\n}\n\n/**\n * Builds a chain-reference-agnostic key (`namespace:address`) for matching the\n * CAIP-10 account IDs we request against the ones the v6 API echoes back.\n *\n * We request EVM balances with the all-chains reference (`eip155:0:<address>`),\n * but the response echoes a separate per-chain ID for every chain\n * (`eip155:1:<address>`, `eip155:137:<address>`, ...). Matching on the full\n * CAIP-10 string therefore fails, so we drop the reference and match on\n * namespace + address instead. EVM addresses are lowercased; other namespaces\n * keep their case.\n *\n * @param caipAccountId - A CAIP-10 account ID.\n * @returns The match key, or a case-normalized fallback if parsing fails.\n */\nfunction toAccountMatchKey(caipAccountId: string): string {\n try {\n const {\n chain: { namespace },\n address,\n } = parseCaipAccountId(caipAccountId as CaipAccountId);\n const normalizedAddress =\n namespace === KnownCaipNamespace.Eip155 ? address.toLowerCase() : address;\n return `${namespace}:${normalizedAddress}`;\n } catch {\n return caipAccountId.startsWith(`${KnownCaipNamespace.Eip155}:`)\n ? caipAccountId.toLowerCase()\n : caipAccountId;\n }\n}\n\n/**\n * Transforms a v6 multiaccount balances response into the stored DeFi state:\n * positions keyed by internal account ID, each mapping to a flat list of\n * protocol groups. Every group carries its own `chainId` (so the client can\n * filter without a nested chain map) plus both the DeFi-tab summary and the\n * details-page sections. Accounts present in the response but with no DeFi\n * positions are included with an empty list so stale data is cleared.\n *\n * When `internalAccountIdByCaip` is provided, response account IDs are matched\n * to internal MetaMask account IDs via namespace + address (ignoring chain\n * reference and EVM case). Unmatched accounts are skipped. When omitted, the\n * response account ID is used as-is (handy for unit tests).\n *\n * @param response - The v6 multiaccount balances response.\n * @param internalAccountIdByCaip - Optional map of request CAIP-10 account IDs\n * to internal MetaMask account IDs.\n * @returns DeFi positions keyed by internal account ID.\n */\nexport function groupDeFiPositionsV6(\n response: V6BalancesResponse,\n internalAccountIdByCaip?: Map<string, string>,\n): DeFiPositionsByAccount {\n const internalAccountIdByMatchKey = internalAccountIdByCaip\n ? new Map(\n [...internalAccountIdByCaip].map(([caipAccountId, internalId]) => [\n toAccountMatchKey(caipAccountId),\n internalId,\n ]),\n )\n : undefined;\n\n // Accumulate groups per resolved internal account ID. The v6 response returns\n // a separate entry per chain (e.g. `eip155:1:<addr>`, `eip155:137:<addr>`),\n // and several of them can resolve to the same internal account ID, so we must\n // merge across all of them rather than overwrite per response account.\n const groupsByAccountKey = new Map<\n string,\n Map<string, DeFiProtocolPositionGroup>\n >();\n\n for (const account of response.accounts) {\n const accountId = internalAccountIdByMatchKey\n ? internalAccountIdByMatchKey.get(toAccountMatchKey(account.accountId))\n : account.accountId;\n if (accountId === undefined) {\n continue;\n }\n\n // Seed every queried account so accounts that no longer hold positions\n // overwrite (clear) any previously stored data.\n let groupsByKey = groupsByAccountKey.get(accountId);\n if (!groupsByKey) {\n groupsByKey = new Map<string, DeFiProtocolPositionGroup>();\n groupsByAccountKey.set(accountId, groupsByKey);\n }\n\n for (const balance of account.balances) {\n if (!isDefiBalanceWithMetadata(balance)) {\n continue;\n }\n\n const position = toUnderlyingPosition(balance);\n const { protocolId, productName, protocolIconUrl } = balance.metadata;\n const groupKey = `${position.chainId}#${protocolId}`;\n\n let group = groupsByKey.get(groupKey);\n if (!group) {\n group = {\n protocolId,\n productName,\n protocolIconUrl,\n chainId: position.chainId,\n marketValue: 0,\n iconGroup: [],\n sections: [],\n };\n groupsByKey.set(groupKey, group);\n }\n\n if (position.marketValue !== undefined) {\n group.marketValue +=\n position.marketValue * getMarketValueSign(position.positionType);\n }\n\n if (!group.iconGroup.some((item) => item.symbol === position.symbol)) {\n group.iconGroup.push({\n symbol: position.symbol,\n avatarValue: position.tokenImage,\n });\n }\n\n // Sections are keyed by productName; distinct groupIds under the same\n // product remain available on each underlying position.\n let section = group.sections.find(\n (item) => item.productName === productName,\n );\n if (!section) {\n section = { productName, positions: [] };\n group.sections.push(section);\n }\n section.positions.push(position);\n }\n }\n\n const result: DeFiPositionsByAccount = {};\n for (const [accountId, groupsByKey] of groupsByAccountKey) {\n const groups = [...groupsByKey.values()];\n for (const group of groups) {\n orderIconGroup(group.iconGroup);\n }\n result[accountId] = groups;\n }\n\n return result;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamask-previews/assets-controllers",
|
|
3
|
-
"version": "111.1.1-preview-
|
|
3
|
+
"version": "111.1.1-preview-429fcf584",
|
|
4
4
|
"description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Ethereum",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"@metamask/polling-controller": "^16.0.9",
|
|
83
83
|
"@metamask/preferences-controller": "^23.1.0",
|
|
84
84
|
"@metamask/profile-sync-controller": "^29.0.0",
|
|
85
|
-
"@metamask/remote-feature-flag-controller": "^
|
|
85
|
+
"@metamask/remote-feature-flag-controller": "^5.0.0",
|
|
86
86
|
"@metamask/rpc-errors": "^7.0.2",
|
|
87
87
|
"@metamask/snaps-controllers": "^19.0.0",
|
|
88
88
|
"@metamask/snaps-sdk": "^11.0.0",
|