@metamask-previews/assets-controllers 110.1.0-preview-ed03d2513 → 110.1.1-preview-abbcfa61d
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 +13 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.cjs.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.d.cts +20 -8
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.d.cts.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.d.mts +20 -8
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.d.mts.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.mjs.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.cjs +134 -37
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.cjs.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.d.cts +39 -10
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.d.cts.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.d.mts +39 -10
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.d.mts.map +1 -1
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.mjs +133 -36
- package/dist/DeFiPositionsController/DeFiPositionsControllerV2.mjs.map +1 -1
- package/dist/DeFiPositionsController/defi-controller-v2-feature-flag.cjs +62 -0
- package/dist/DeFiPositionsController/defi-controller-v2-feature-flag.cjs.map +1 -0
- package/dist/DeFiPositionsController/defi-controller-v2-feature-flag.d.cts +27 -0
- package/dist/DeFiPositionsController/defi-controller-v2-feature-flag.d.cts.map +1 -0
- package/dist/DeFiPositionsController/defi-controller-v2-feature-flag.d.mts +27 -0
- package/dist/DeFiPositionsController/defi-controller-v2-feature-flag.d.mts.map +1 -0
- package/dist/DeFiPositionsController/defi-controller-v2-feature-flag.mjs +58 -0
- package/dist/DeFiPositionsController/defi-controller-v2-feature-flag.mjs.map +1 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -9,8 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
### Changed
|
|
11
11
|
|
|
12
|
+
- **BREAKING:** `DeFiPositionsControllerV2.fetchDeFiPositions` now polls while any selected account has `processingDefiPositions: true`, updating state only when every account is ready, invalidating the balances cache between attempts, sharing one in-flight promise per selected-account + `vsCurrency` key (so fast switches can join an earlier matching poll), and stopping on request failure or the max attempt limit ([#9711](https://github.com/MetaMask/core/pull/9711))
|
|
13
|
+
- Clients must allow and delegate `RemoteFeatureFlagController:getState` on the `DeFiPositionsControllerV2` messenger.
|
|
14
|
+
- Bump `@metamask/transaction-controller` from `^69.4.0` to `^69.5.0` ([#9780](https://github.com/MetaMask/core/pull/9780))
|
|
15
|
+
|
|
16
|
+
## [110.1.1]
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Bump `@metamask/core-backend` from `^8.1.0` to `^8.1.1` ([#9779](https://github.com/MetaMask/core/pull/9779))
|
|
21
|
+
- Bump `@metamask/account-tree-controller` from `^7.5.5` to `^7.6.0` ([#9779](https://github.com/MetaMask/core/pull/9779))
|
|
12
22
|
- Bump `@metamask/network-controller` from `^35.0.0` to `^35.0.1` ([#9758](https://github.com/MetaMask/core/pull/9758))
|
|
13
23
|
- Bump `@metamask/phishing-controller` from `^17.3.0` to `^17.3.1` ([#9746](https://github.com/MetaMask/core/pull/9746))
|
|
24
|
+
- Bump `@metamask/profile-sync-controller` from `^28.3.0` to `^29.0.0` ([#9779](https://github.com/MetaMask/core/pull/9779))
|
|
14
25
|
|
|
15
26
|
## [110.1.0]
|
|
16
27
|
|
|
@@ -3379,7 +3390,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
3379
3390
|
|
|
3380
3391
|
- Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845))
|
|
3381
3392
|
|
|
3382
|
-
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@110.1.
|
|
3393
|
+
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@110.1.1...HEAD
|
|
3394
|
+
[110.1.1]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@110.1.0...@metamask/assets-controllers@110.1.1
|
|
3383
3395
|
[110.1.0]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@110.0.3...@metamask/assets-controllers@110.1.0
|
|
3384
3396
|
[110.0.3]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@110.0.2...@metamask/assets-controllers@110.0.3
|
|
3385
3397
|
[110.0.2]: https://github.com/MetaMask/core/compare/@metamask/assets-controllers@110.0.1...@metamask/assets-controllers@110.0.2
|
|
@@ -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.
|
|
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,18 +4,30 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { DeFiPositionsControllerV2 } from "./DeFiPositionsControllerV2.cjs";
|
|
6
6
|
/**
|
|
7
|
-
* Fetches DeFi positions for the selected account group.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* state
|
|
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
|
+
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
21
|
+
* No-ops when disabled or when the group has no
|
|
11
22
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
12
23
|
* TanStack Query cache (keyed by accounts + query options including
|
|
13
|
-
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache
|
|
14
|
-
* pull-to-refresh).
|
|
24
|
+
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the
|
|
25
|
+
* first attempt (e.g. pull-to-refresh).
|
|
15
26
|
*
|
|
16
27
|
* @param options - Optional fetch modifiers.
|
|
17
|
-
* @param options.forceRefresh - When true, bypass the apiClient cache
|
|
18
|
-
* fetch immediately.
|
|
28
|
+
* @param options.forceRefresh - When true, bypass the apiClient cache on the
|
|
29
|
+
* first attempt and fetch immediately.
|
|
30
|
+
* @returns Resolves when the fetch (and any processing polls) finish.
|
|
19
31
|
*/
|
|
20
32
|
export type DeFiPositionsControllerV2FetchDeFiPositionsAction = {
|
|
21
33
|
type: `DeFiPositionsControllerV2:fetchDeFiPositions`;
|
package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeFiPositionsControllerV2-method-action-types.d.cts","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,wCAAuC;AAEhF
|
|
1
|
+
{"version":3,"file":"DeFiPositionsControllerV2-method-action-types.d.cts","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,wCAAuC;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,MAAM,iDAAiD,GAAG;IAC9D,IAAI,EAAE,8CAA8C,CAAC;IACrD,OAAO,EAAE,yBAAyB,CAAC,oBAAoB,CAAC,CAAC;CAC1D,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sCAAsC,GAChD,iDAAiD,CAAC"}
|
|
@@ -4,18 +4,30 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { DeFiPositionsControllerV2 } from "./DeFiPositionsControllerV2.mjs";
|
|
6
6
|
/**
|
|
7
|
-
* Fetches DeFi positions for the selected account group.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* state
|
|
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
|
+
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
21
|
+
* No-ops when disabled or when the group has no
|
|
11
22
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
12
23
|
* TanStack Query cache (keyed by accounts + query options including
|
|
13
|
-
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache
|
|
14
|
-
* pull-to-refresh).
|
|
24
|
+
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the
|
|
25
|
+
* first attempt (e.g. pull-to-refresh).
|
|
15
26
|
*
|
|
16
27
|
* @param options - Optional fetch modifiers.
|
|
17
|
-
* @param options.forceRefresh - When true, bypass the apiClient cache
|
|
18
|
-
* fetch immediately.
|
|
28
|
+
* @param options.forceRefresh - When true, bypass the apiClient cache on the
|
|
29
|
+
* first attempt and fetch immediately.
|
|
30
|
+
* @returns Resolves when the fetch (and any processing polls) finish.
|
|
19
31
|
*/
|
|
20
32
|
export type DeFiPositionsControllerV2FetchDeFiPositionsAction = {
|
|
21
33
|
type: `DeFiPositionsControllerV2:fetchDeFiPositions`;
|
package/dist/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeFiPositionsControllerV2-method-action-types.d.mts","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,wCAAuC;AAEhF
|
|
1
|
+
{"version":3,"file":"DeFiPositionsControllerV2-method-action-types.d.mts","sourceRoot":"","sources":["../../src/DeFiPositionsController/DeFiPositionsControllerV2-method-action-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,yBAAyB,EAAE,wCAAuC;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,MAAM,iDAAiD,GAAG;IAC9D,IAAI,EAAE,8CAA8C,CAAC;IACrD,OAAO,EAAE,yBAAyB,CAAC,oBAAoB,CAAC,CAAC;CAC1D,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sCAAsC,GAChD,iDAAiD,CAAC"}
|
|
@@ -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.
|
|
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"]}
|
|
@@ -10,14 +10,22 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
10
10
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
11
11
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
12
12
|
};
|
|
13
|
-
var _DeFiPositionsControllerV2_apiClient, _DeFiPositionsControllerV2_isEnabled, _DeFiPositionsControllerV2_getVsCurrency;
|
|
13
|
+
var _DeFiPositionsControllerV2_instances, _DeFiPositionsControllerV2_apiClient, _DeFiPositionsControllerV2_isEnabled, _DeFiPositionsControllerV2_getVsCurrency, _DeFiPositionsControllerV2_inFlightFetches, _DeFiPositionsControllerV2_fetchDeFiPositions;
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.DeFiPositionsControllerV2 = exports.getDefaultDeFiPositionsControllerV2State = void 0;
|
|
16
16
|
const base_controller_1 = require("@metamask/base-controller");
|
|
17
17
|
const build_defi_balances_query_js_1 = require("./build-defi-balances-query.cjs");
|
|
18
|
+
const defi_controller_v2_feature_flag_js_1 = require("./defi-controller-v2-feature-flag.cjs");
|
|
18
19
|
const group_defi_positions_v6_js_1 = require("./group-defi-positions-v6.cjs");
|
|
19
20
|
const controllerName = 'DeFiPositionsControllerV2';
|
|
20
21
|
const MESSENGER_EXPOSED_METHODS = ['fetchDeFiPositions'];
|
|
22
|
+
/**
|
|
23
|
+
* @param ms - Milliseconds to wait.
|
|
24
|
+
* @returns A promise that resolves after `ms`.
|
|
25
|
+
*/
|
|
26
|
+
function delay(ms) {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
21
29
|
const controllerMetadata = {
|
|
22
30
|
allDeFiPositionsV2: {
|
|
23
31
|
includeInStateLogs: false,
|
|
@@ -39,7 +47,23 @@ exports.getDefaultDeFiPositionsControllerV2State = getDefaultDeFiPositionsContro
|
|
|
39
47
|
*
|
|
40
48
|
* Deduplication and freshness are handled by the shared TanStack Query cache on
|
|
41
49
|
* {@link ApiPlatformClient} (balances default `staleTime` is 1 minute). Pass
|
|
42
|
-
* `{ forceRefresh: true }` to bypass that cache
|
|
50
|
+
* `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.
|
|
51
|
+
* pull-to-refresh); later processing polls always bypass the cache.
|
|
52
|
+
*
|
|
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.
|
|
61
|
+
*
|
|
62
|
+
* Processing-poll `maxAttempts` / `pollInterval` are read via
|
|
63
|
+
* {@link getProcessingPollConfig} from the `defiControllerV2` remote feature
|
|
64
|
+
* flag (`RemoteFeatureFlagController:getState`), falling back to built-in
|
|
65
|
+
* defaults when unset or invalid. Clients must delegate that action to this
|
|
66
|
+
* controller's messenger.
|
|
43
67
|
*/
|
|
44
68
|
class DeFiPositionsControllerV2 extends base_controller_1.BaseController {
|
|
45
69
|
/**
|
|
@@ -60,69 +84,142 @@ class DeFiPositionsControllerV2 extends base_controller_1.BaseController {
|
|
|
60
84
|
...state,
|
|
61
85
|
},
|
|
62
86
|
});
|
|
87
|
+
_DeFiPositionsControllerV2_instances.add(this);
|
|
63
88
|
_DeFiPositionsControllerV2_apiClient.set(this, void 0);
|
|
64
89
|
_DeFiPositionsControllerV2_isEnabled.set(this, void 0);
|
|
65
90
|
_DeFiPositionsControllerV2_getVsCurrency.set(this, void 0);
|
|
91
|
+
/**
|
|
92
|
+
* In-flight fetches keyed by selected DeFi-queryable account IDs plus
|
|
93
|
+
* `vsCurrency`. Concurrent callers for the same selection and currency share
|
|
94
|
+
* a promise; different selections or fiat currencies keep independent
|
|
95
|
+
* fetches so fast switching can join an earlier matching poll.
|
|
96
|
+
*/
|
|
97
|
+
_DeFiPositionsControllerV2_inFlightFetches.set(this, new Map());
|
|
66
98
|
__classPrivateFieldSet(this, _DeFiPositionsControllerV2_apiClient, apiClient, "f");
|
|
67
99
|
__classPrivateFieldSet(this, _DeFiPositionsControllerV2_isEnabled, isEnabled, "f");
|
|
68
100
|
__classPrivateFieldSet(this, _DeFiPositionsControllerV2_getVsCurrency, getVsCurrency, "f");
|
|
69
101
|
this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
|
|
70
102
|
}
|
|
71
103
|
/**
|
|
72
|
-
* Fetches DeFi positions for the selected account group.
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* state
|
|
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` /
|
|
117
|
+
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
118
|
+
* No-ops when disabled or when the group has no
|
|
76
119
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
77
120
|
* TanStack Query cache (keyed by accounts + query options including
|
|
78
|
-
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache
|
|
79
|
-
* pull-to-refresh).
|
|
121
|
+
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the
|
|
122
|
+
* first attempt (e.g. pull-to-refresh).
|
|
80
123
|
*
|
|
81
124
|
* @param options - Optional fetch modifiers.
|
|
82
|
-
* @param options.forceRefresh - When true, bypass the apiClient cache
|
|
83
|
-
* fetch immediately.
|
|
125
|
+
* @param options.forceRefresh - When true, bypass the apiClient cache on the
|
|
126
|
+
* first attempt and fetch immediately.
|
|
127
|
+
* @returns Resolves when the fetch (and any processing polls) finish.
|
|
84
128
|
*/
|
|
85
129
|
async fetchDeFiPositions(options) {
|
|
86
130
|
if (!__classPrivateFieldGet(this, _DeFiPositionsControllerV2_isEnabled, "f").call(this)) {
|
|
87
131
|
return;
|
|
88
132
|
}
|
|
89
133
|
const selectedAccounts = this.messenger.call('AccountTreeController:getAccountsFromSelectedAccountGroup');
|
|
90
|
-
const
|
|
91
|
-
|
|
134
|
+
const query = (0, build_defi_balances_query_js_1.buildDeFiBalancesQuery)(selectedAccounts);
|
|
135
|
+
const vsCurrency = __classPrivateFieldGet(this, _DeFiPositionsControllerV2_getVsCurrency, "f").call(this).toLowerCase();
|
|
136
|
+
// Include vsCurrency so a fiat change does not join a poll priced in the
|
|
137
|
+
// previous currency (TanStack also keys the balances cache on vsCurrency).
|
|
138
|
+
const inFlightKey = [
|
|
139
|
+
...[...query.internalAccountIdByCaip.keys()].sort(),
|
|
140
|
+
vsCurrency,
|
|
141
|
+
].join('\0');
|
|
142
|
+
const existing = __classPrivateFieldGet(this, _DeFiPositionsControllerV2_inFlightFetches, "f").get(inFlightKey);
|
|
143
|
+
if (existing) {
|
|
144
|
+
await existing;
|
|
92
145
|
return;
|
|
93
146
|
}
|
|
94
|
-
|
|
95
|
-
|
|
147
|
+
// Same-key callers join this promise instead of replacing it, so cleanup
|
|
148
|
+
// can always delete without checking identity.
|
|
149
|
+
const fetchPromise = __classPrivateFieldGet(this, _DeFiPositionsControllerV2_instances, "m", _DeFiPositionsControllerV2_fetchDeFiPositions).call(this, options, query, vsCurrency).finally(() => {
|
|
150
|
+
__classPrivateFieldGet(this, _DeFiPositionsControllerV2_inFlightFetches, "f").delete(inFlightKey);
|
|
151
|
+
});
|
|
152
|
+
__classPrivateFieldGet(this, _DeFiPositionsControllerV2_inFlightFetches, "f").set(inFlightKey, fetchPromise);
|
|
153
|
+
await fetchPromise;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.DeFiPositionsControllerV2 = DeFiPositionsControllerV2;
|
|
157
|
+
_DeFiPositionsControllerV2_apiClient = new WeakMap(), _DeFiPositionsControllerV2_isEnabled = new WeakMap(), _DeFiPositionsControllerV2_getVsCurrency = new WeakMap(), _DeFiPositionsControllerV2_inFlightFetches = new WeakMap(), _DeFiPositionsControllerV2_instances = new WeakSet(), _DeFiPositionsControllerV2_fetchDeFiPositions = async function _DeFiPositionsControllerV2_fetchDeFiPositions(options, { networks, internalAccountIdByCaip }, vsCurrency) {
|
|
158
|
+
if (internalAccountIdByCaip.size === 0 || networks.length === 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const accountIds = [...internalAccountIdByCaip.keys()];
|
|
162
|
+
const queryOptions = {
|
|
163
|
+
networks,
|
|
164
|
+
includeDeFiBalances: true,
|
|
165
|
+
forceFetchDeFiPositions: true,
|
|
166
|
+
includePrices: true,
|
|
167
|
+
vsCurrency,
|
|
168
|
+
};
|
|
169
|
+
// Resolve once per fetch so a mid-poll remote-flag change cannot stretch
|
|
170
|
+
// or shrink this poll sequence inconsistently.
|
|
171
|
+
const { maxAttempts, pollInterval } = (0, defi_controller_v2_feature_flag_js_1.getProcessingPollConfig)(this.messenger);
|
|
172
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
173
|
+
// First attempt respects forceRefresh; later polls always bypass cache
|
|
174
|
+
// so we do not spin on a stale processing snapshot.
|
|
175
|
+
const fetchOptions = {
|
|
176
|
+
...(options?.forceRefresh || attempt > 0 ? { staleTime: 0 } : {}),
|
|
177
|
+
};
|
|
178
|
+
let response;
|
|
96
179
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const positionsByAccount = (0, group_defi_positions_v6_js_1.groupDeFiPositionsV6)({ ...response, accounts: readyAccounts }, internalAccountIdByCaip);
|
|
113
|
-
// Last valid response wins per ready account; processing / other accounts
|
|
114
|
-
// stay untouched.
|
|
180
|
+
response = await __classPrivateFieldGet(this, _DeFiPositionsControllerV2_apiClient, "f").accounts.fetchV6MultiAccountBalances(accountIds, queryOptions, fetchOptions);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
// Soft-fail so prior state stays and the in-flight promise settles.
|
|
184
|
+
console.error('Failed to fetch DeFi positions', error);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
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.
|
|
191
|
+
if (!stillProcessing) {
|
|
192
|
+
const positionsByAccount = (0, group_defi_positions_v6_js_1.groupDeFiPositionsV6)(response, internalAccountIdByCaip);
|
|
115
193
|
this.update((state) => {
|
|
116
194
|
for (const [accountId, positions] of Object.entries(positionsByAccount)) {
|
|
117
195
|
state.allDeFiPositionsV2[accountId] = positions;
|
|
118
196
|
}
|
|
119
197
|
});
|
|
198
|
+
// Report how many attempts were needed (only when polling was
|
|
199
|
+
// required) so Sentry can inform remote-flag / default poll-limit
|
|
200
|
+
// tuning without flooding on first-try successes.
|
|
201
|
+
const attemptsTaken = attempt + 1;
|
|
202
|
+
if (attemptsTaken > 1) {
|
|
203
|
+
const multipleAttemptsError = new Error(`DeFiPositionsControllerV2: positions ready after ${attemptsTaken} attempt(s)`);
|
|
204
|
+
multipleAttemptsError.name = 'DeFiPositionsV2FetchAttempts';
|
|
205
|
+
this.messenger.captureException?.(multipleAttemptsError);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
120
208
|
}
|
|
121
|
-
|
|
122
|
-
|
|
209
|
+
const { queryKey } = __classPrivateFieldGet(this, _DeFiPositionsControllerV2_apiClient, "f").accounts.getV6MultiAccountBalancesQueryOptions(accountIds, queryOptions, fetchOptions);
|
|
210
|
+
await __classPrivateFieldGet(this, _DeFiPositionsControllerV2_apiClient, "f").accounts.queryClient.invalidateQueries({
|
|
211
|
+
queryKey,
|
|
212
|
+
});
|
|
213
|
+
const isLastAttempt = attempt >= maxAttempts - 1;
|
|
214
|
+
if (isLastAttempt) {
|
|
215
|
+
// Report exhausted polls so Sentry can inform remote-flag / default
|
|
216
|
+
// poll-limit tuning when indexing never finishes in time.
|
|
217
|
+
const multipleAttemptsError = new Error(`DeFiPositionsControllerV2: still processing after ${maxAttempts} attempt(s)`);
|
|
218
|
+
multipleAttemptsError.name = 'DeFiPositionsV2ProcessingPollExhausted';
|
|
219
|
+
this.messenger.captureException?.(multipleAttemptsError);
|
|
220
|
+
return;
|
|
123
221
|
}
|
|
222
|
+
await delay(pollInterval);
|
|
124
223
|
}
|
|
125
|
-
}
|
|
126
|
-
exports.DeFiPositionsControllerV2 = DeFiPositionsControllerV2;
|
|
127
|
-
_DeFiPositionsControllerV2_apiClient = new WeakMap(), _DeFiPositionsControllerV2_isEnabled = new WeakMap(), _DeFiPositionsControllerV2_getVsCurrency = new WeakMap();
|
|
224
|
+
};
|
|
128
225
|
//# sourceMappingURL=DeFiPositionsControllerV2.cjs.map
|
|
@@ -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';\n\nimport { buildDeFiBalancesQuery } from './build-defi-balances-query.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\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\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 for pull-to-refresh.\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 * @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. Each account key in\n * a ready response replaces that account's state (other accounts stay).\n * Accounts still indexing (`processingDefiPositions`) are skipped so prior\n * state is kept for them. 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 (e.g.\n * pull-to-refresh).\n *\n * @param options - Optional fetch modifiers.\n * @param options.forceRefresh - When true, bypass the apiClient cache and\n * fetch immediately.\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\n const { networks, internalAccountIdByCaip } =\n buildDeFiBalancesQuery(selectedAccounts);\n\n if (internalAccountIdByCaip.size === 0 || networks.length === 0) {\n return;\n }\n\n const accountIds = [...internalAccountIdByCaip.keys()];\n const vsCurrency = this.#getVsCurrency().toLowerCase();\n\n try {\n const response =\n await this.#apiClient.accounts.fetchV6MultiAccountBalances(\n accountIds,\n {\n networks,\n includeDeFiBalances: true,\n forceFetchDeFiPositions: true,\n includePrices: true,\n vsCurrency,\n },\n {\n // staleTime: 0 makes TanStack treat the cache as stale for this call.\n ...(options?.forceRefresh ? { staleTime: 0 } : {}),\n },\n );\n\n // Skip accounts still indexing — their balances are not a valid snapshot.\n const readyAccounts = response.accounts.filter(\n (account) => !account.processingDefiPositions,\n );\n if (readyAccounts.length === 0) {\n return;\n }\n\n const positionsByAccount = groupDeFiPositionsV6(\n { ...response, accounts: readyAccounts },\n internalAccountIdByCaip,\n );\n\n // Last valid response wins per ready account; processing / other accounts\n // stay untouched.\n this.update((state) => {\n for (const [accountId, positions] of Object.entries(\n positionsByAccount,\n )) {\n state.allDeFiPositionsV2[accountId] = positions;\n }\n });\n } catch (error) {\n console.error('Failed to fetch DeFi positions', error);\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"]}
|
|
@@ -3,6 +3,7 @@ import { BaseController } from "@metamask/base-controller";
|
|
|
3
3
|
import type { ControllerGetStateAction, ControllerStateChangedEvent } from "@metamask/base-controller";
|
|
4
4
|
import type { ApiPlatformClient } from "@metamask/core-backend";
|
|
5
5
|
import type { Messenger } from "@metamask/messenger";
|
|
6
|
+
import type { RemoteFeatureFlagControllerGetStateAction } from "@metamask/remote-feature-flag-controller";
|
|
6
7
|
import type { DeFiPositionsControllerV2MethodActions } from "./DeFiPositionsControllerV2-method-action-types.cjs";
|
|
7
8
|
import type { DeFiPositionsByAccount } from "./group-defi-positions-v6.cjs";
|
|
8
9
|
declare const controllerName = "DeFiPositionsControllerV2";
|
|
@@ -30,7 +31,7 @@ export type DeFiPositionsControllerV2Events = DeFiPositionsControllerV2StateChan
|
|
|
30
31
|
/**
|
|
31
32
|
* The external actions available to the {@link DeFiPositionsControllerV2}.
|
|
32
33
|
*/
|
|
33
|
-
export type AllowedActions = AccountTreeControllerGetAccountsFromSelectedAccountGroupAction;
|
|
34
|
+
export type AllowedActions = AccountTreeControllerGetAccountsFromSelectedAccountGroupAction | RemoteFeatureFlagControllerGetStateAction;
|
|
34
35
|
/**
|
|
35
36
|
* The external events available to the {@link DeFiPositionsControllerV2}.
|
|
36
37
|
*
|
|
@@ -48,7 +49,23 @@ export type DeFiPositionsControllerV2Messenger = Messenger<typeof controllerName
|
|
|
48
49
|
*
|
|
49
50
|
* Deduplication and freshness are handled by the shared TanStack Query cache on
|
|
50
51
|
* {@link ApiPlatformClient} (balances default `staleTime` is 1 minute). Pass
|
|
51
|
-
* `{ forceRefresh: true }` to bypass that cache
|
|
52
|
+
* `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.
|
|
53
|
+
* pull-to-refresh); later processing polls always bypass the cache.
|
|
54
|
+
*
|
|
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.
|
|
63
|
+
*
|
|
64
|
+
* Processing-poll `maxAttempts` / `pollInterval` are read via
|
|
65
|
+
* {@link getProcessingPollConfig} from the `defiControllerV2` remote feature
|
|
66
|
+
* flag (`RemoteFeatureFlagController:getState`), falling back to built-in
|
|
67
|
+
* defaults when unset or invalid. Clients must delegate that action to this
|
|
68
|
+
* controller's messenger.
|
|
52
69
|
*/
|
|
53
70
|
export declare class DeFiPositionsControllerV2 extends BaseController<typeof controllerName, DeFiPositionsControllerV2State, DeFiPositionsControllerV2Messenger> {
|
|
54
71
|
#private;
|
|
@@ -68,18 +85,30 @@ export declare class DeFiPositionsControllerV2 extends BaseController<typeof con
|
|
|
68
85
|
state?: Partial<DeFiPositionsControllerV2State>;
|
|
69
86
|
});
|
|
70
87
|
/**
|
|
71
|
-
* Fetches DeFi positions for the selected account group.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* state
|
|
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` /
|
|
101
|
+
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
102
|
+
* No-ops when disabled or when the group has no
|
|
75
103
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
76
104
|
* TanStack Query cache (keyed by accounts + query options including
|
|
77
|
-
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache
|
|
78
|
-
* pull-to-refresh).
|
|
105
|
+
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the
|
|
106
|
+
* first attempt (e.g. pull-to-refresh).
|
|
79
107
|
*
|
|
80
108
|
* @param options - Optional fetch modifiers.
|
|
81
|
-
* @param options.forceRefresh - When true, bypass the apiClient cache
|
|
82
|
-
* fetch immediately.
|
|
109
|
+
* @param options.forceRefresh - When true, bypass the apiClient cache on the
|
|
110
|
+
* first attempt and fetch immediately.
|
|
111
|
+
* @returns Resolves when the fetch (and any processing polls) finish.
|
|
83
112
|
*/
|
|
84
113
|
fetchDeFiPositions(options?: {
|
|
85
114
|
forceRefresh?: boolean;
|
|
@@ -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;
|
|
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"}
|
|
@@ -3,6 +3,7 @@ import { BaseController } from "@metamask/base-controller";
|
|
|
3
3
|
import type { ControllerGetStateAction, ControllerStateChangedEvent } from "@metamask/base-controller";
|
|
4
4
|
import type { ApiPlatformClient } from "@metamask/core-backend";
|
|
5
5
|
import type { Messenger } from "@metamask/messenger";
|
|
6
|
+
import type { RemoteFeatureFlagControllerGetStateAction } from "@metamask/remote-feature-flag-controller";
|
|
6
7
|
import type { DeFiPositionsControllerV2MethodActions } from "./DeFiPositionsControllerV2-method-action-types.mjs";
|
|
7
8
|
import type { DeFiPositionsByAccount } from "./group-defi-positions-v6.mjs";
|
|
8
9
|
declare const controllerName = "DeFiPositionsControllerV2";
|
|
@@ -30,7 +31,7 @@ export type DeFiPositionsControllerV2Events = DeFiPositionsControllerV2StateChan
|
|
|
30
31
|
/**
|
|
31
32
|
* The external actions available to the {@link DeFiPositionsControllerV2}.
|
|
32
33
|
*/
|
|
33
|
-
export type AllowedActions = AccountTreeControllerGetAccountsFromSelectedAccountGroupAction;
|
|
34
|
+
export type AllowedActions = AccountTreeControllerGetAccountsFromSelectedAccountGroupAction | RemoteFeatureFlagControllerGetStateAction;
|
|
34
35
|
/**
|
|
35
36
|
* The external events available to the {@link DeFiPositionsControllerV2}.
|
|
36
37
|
*
|
|
@@ -48,7 +49,23 @@ export type DeFiPositionsControllerV2Messenger = Messenger<typeof controllerName
|
|
|
48
49
|
*
|
|
49
50
|
* Deduplication and freshness are handled by the shared TanStack Query cache on
|
|
50
51
|
* {@link ApiPlatformClient} (balances default `staleTime` is 1 minute). Pass
|
|
51
|
-
* `{ forceRefresh: true }` to bypass that cache
|
|
52
|
+
* `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.
|
|
53
|
+
* pull-to-refresh); later processing polls always bypass the cache.
|
|
54
|
+
*
|
|
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.
|
|
63
|
+
*
|
|
64
|
+
* Processing-poll `maxAttempts` / `pollInterval` are read via
|
|
65
|
+
* {@link getProcessingPollConfig} from the `defiControllerV2` remote feature
|
|
66
|
+
* flag (`RemoteFeatureFlagController:getState`), falling back to built-in
|
|
67
|
+
* defaults when unset or invalid. Clients must delegate that action to this
|
|
68
|
+
* controller's messenger.
|
|
52
69
|
*/
|
|
53
70
|
export declare class DeFiPositionsControllerV2 extends BaseController<typeof controllerName, DeFiPositionsControllerV2State, DeFiPositionsControllerV2Messenger> {
|
|
54
71
|
#private;
|
|
@@ -68,18 +85,30 @@ export declare class DeFiPositionsControllerV2 extends BaseController<typeof con
|
|
|
68
85
|
state?: Partial<DeFiPositionsControllerV2State>;
|
|
69
86
|
});
|
|
70
87
|
/**
|
|
71
|
-
* Fetches DeFi positions for the selected account group.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* state
|
|
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` /
|
|
101
|
+
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
102
|
+
* No-ops when disabled or when the group has no
|
|
75
103
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
76
104
|
* TanStack Query cache (keyed by accounts + query options including
|
|
77
|
-
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache
|
|
78
|
-
* pull-to-refresh).
|
|
105
|
+
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the
|
|
106
|
+
* first attempt (e.g. pull-to-refresh).
|
|
79
107
|
*
|
|
80
108
|
* @param options - Optional fetch modifiers.
|
|
81
|
-
* @param options.forceRefresh - When true, bypass the apiClient cache
|
|
82
|
-
* fetch immediately.
|
|
109
|
+
* @param options.forceRefresh - When true, bypass the apiClient cache on the
|
|
110
|
+
* first attempt and fetch immediately.
|
|
111
|
+
* @returns Resolves when the fetch (and any processing polls) finish.
|
|
83
112
|
*/
|
|
84
113
|
fetchDeFiPositions(options?: {
|
|
85
114
|
forceRefresh?: boolean;
|
|
@@ -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;
|
|
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"}
|
|
@@ -9,12 +9,20 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
|
-
var _DeFiPositionsControllerV2_apiClient, _DeFiPositionsControllerV2_isEnabled, _DeFiPositionsControllerV2_getVsCurrency;
|
|
12
|
+
var _DeFiPositionsControllerV2_instances, _DeFiPositionsControllerV2_apiClient, _DeFiPositionsControllerV2_isEnabled, _DeFiPositionsControllerV2_getVsCurrency, _DeFiPositionsControllerV2_inFlightFetches, _DeFiPositionsControllerV2_fetchDeFiPositions;
|
|
13
13
|
import { BaseController } from "@metamask/base-controller";
|
|
14
14
|
import { buildDeFiBalancesQuery } from "./build-defi-balances-query.mjs";
|
|
15
|
+
import { getProcessingPollConfig } from "./defi-controller-v2-feature-flag.mjs";
|
|
15
16
|
import { groupDeFiPositionsV6 } from "./group-defi-positions-v6.mjs";
|
|
16
17
|
const controllerName = 'DeFiPositionsControllerV2';
|
|
17
18
|
const MESSENGER_EXPOSED_METHODS = ['fetchDeFiPositions'];
|
|
19
|
+
/**
|
|
20
|
+
* @param ms - Milliseconds to wait.
|
|
21
|
+
* @returns A promise that resolves after `ms`.
|
|
22
|
+
*/
|
|
23
|
+
function delay(ms) {
|
|
24
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
+
}
|
|
18
26
|
const controllerMetadata = {
|
|
19
27
|
allDeFiPositionsV2: {
|
|
20
28
|
includeInStateLogs: false,
|
|
@@ -35,7 +43,23 @@ export const getDefaultDeFiPositionsControllerV2State = () => {
|
|
|
35
43
|
*
|
|
36
44
|
* Deduplication and freshness are handled by the shared TanStack Query cache on
|
|
37
45
|
* {@link ApiPlatformClient} (balances default `staleTime` is 1 minute). Pass
|
|
38
|
-
* `{ forceRefresh: true }` to bypass that cache
|
|
46
|
+
* `{ forceRefresh: true }` to bypass that cache on the first attempt (e.g.
|
|
47
|
+
* pull-to-refresh); later processing polls always bypass the cache.
|
|
48
|
+
*
|
|
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.
|
|
57
|
+
*
|
|
58
|
+
* Processing-poll `maxAttempts` / `pollInterval` are read via
|
|
59
|
+
* {@link getProcessingPollConfig} from the `defiControllerV2` remote feature
|
|
60
|
+
* flag (`RemoteFeatureFlagController:getState`), falling back to built-in
|
|
61
|
+
* defaults when unset or invalid. Clients must delegate that action to this
|
|
62
|
+
* controller's messenger.
|
|
39
63
|
*/
|
|
40
64
|
export class DeFiPositionsControllerV2 extends BaseController {
|
|
41
65
|
/**
|
|
@@ -56,68 +80,141 @@ export class DeFiPositionsControllerV2 extends BaseController {
|
|
|
56
80
|
...state,
|
|
57
81
|
},
|
|
58
82
|
});
|
|
83
|
+
_DeFiPositionsControllerV2_instances.add(this);
|
|
59
84
|
_DeFiPositionsControllerV2_apiClient.set(this, void 0);
|
|
60
85
|
_DeFiPositionsControllerV2_isEnabled.set(this, void 0);
|
|
61
86
|
_DeFiPositionsControllerV2_getVsCurrency.set(this, void 0);
|
|
87
|
+
/**
|
|
88
|
+
* In-flight fetches keyed by selected DeFi-queryable account IDs plus
|
|
89
|
+
* `vsCurrency`. Concurrent callers for the same selection and currency share
|
|
90
|
+
* a promise; different selections or fiat currencies keep independent
|
|
91
|
+
* fetches so fast switching can join an earlier matching poll.
|
|
92
|
+
*/
|
|
93
|
+
_DeFiPositionsControllerV2_inFlightFetches.set(this, new Map());
|
|
62
94
|
__classPrivateFieldSet(this, _DeFiPositionsControllerV2_apiClient, apiClient, "f");
|
|
63
95
|
__classPrivateFieldSet(this, _DeFiPositionsControllerV2_isEnabled, isEnabled, "f");
|
|
64
96
|
__classPrivateFieldSet(this, _DeFiPositionsControllerV2_getVsCurrency, getVsCurrency, "f");
|
|
65
97
|
this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
|
|
66
98
|
}
|
|
67
99
|
/**
|
|
68
|
-
* Fetches DeFi positions for the selected account group.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* state
|
|
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` /
|
|
113
|
+
* `DeFiPositionsV2ProcessingPollExhausted`) so poll limits can be tuned.
|
|
114
|
+
* No-ops when disabled or when the group has no
|
|
72
115
|
* supported accounts. Caching / spam prevention is handled by the apiClient
|
|
73
116
|
* TanStack Query cache (keyed by accounts + query options including
|
|
74
|
-
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache
|
|
75
|
-
* pull-to-refresh).
|
|
117
|
+
* `vsCurrency`). Pass `{ forceRefresh: true }` to bypass the cache on the
|
|
118
|
+
* first attempt (e.g. pull-to-refresh).
|
|
76
119
|
*
|
|
77
120
|
* @param options - Optional fetch modifiers.
|
|
78
|
-
* @param options.forceRefresh - When true, bypass the apiClient cache
|
|
79
|
-
* fetch immediately.
|
|
121
|
+
* @param options.forceRefresh - When true, bypass the apiClient cache on the
|
|
122
|
+
* first attempt and fetch immediately.
|
|
123
|
+
* @returns Resolves when the fetch (and any processing polls) finish.
|
|
80
124
|
*/
|
|
81
125
|
async fetchDeFiPositions(options) {
|
|
82
126
|
if (!__classPrivateFieldGet(this, _DeFiPositionsControllerV2_isEnabled, "f").call(this)) {
|
|
83
127
|
return;
|
|
84
128
|
}
|
|
85
129
|
const selectedAccounts = this.messenger.call('AccountTreeController:getAccountsFromSelectedAccountGroup');
|
|
86
|
-
const
|
|
87
|
-
|
|
130
|
+
const query = buildDeFiBalancesQuery(selectedAccounts);
|
|
131
|
+
const vsCurrency = __classPrivateFieldGet(this, _DeFiPositionsControllerV2_getVsCurrency, "f").call(this).toLowerCase();
|
|
132
|
+
// Include vsCurrency so a fiat change does not join a poll priced in the
|
|
133
|
+
// previous currency (TanStack also keys the balances cache on vsCurrency).
|
|
134
|
+
const inFlightKey = [
|
|
135
|
+
...[...query.internalAccountIdByCaip.keys()].sort(),
|
|
136
|
+
vsCurrency,
|
|
137
|
+
].join('\0');
|
|
138
|
+
const existing = __classPrivateFieldGet(this, _DeFiPositionsControllerV2_inFlightFetches, "f").get(inFlightKey);
|
|
139
|
+
if (existing) {
|
|
140
|
+
await existing;
|
|
88
141
|
return;
|
|
89
142
|
}
|
|
90
|
-
|
|
91
|
-
|
|
143
|
+
// Same-key callers join this promise instead of replacing it, so cleanup
|
|
144
|
+
// can always delete without checking identity.
|
|
145
|
+
const fetchPromise = __classPrivateFieldGet(this, _DeFiPositionsControllerV2_instances, "m", _DeFiPositionsControllerV2_fetchDeFiPositions).call(this, options, query, vsCurrency).finally(() => {
|
|
146
|
+
__classPrivateFieldGet(this, _DeFiPositionsControllerV2_inFlightFetches, "f").delete(inFlightKey);
|
|
147
|
+
});
|
|
148
|
+
__classPrivateFieldGet(this, _DeFiPositionsControllerV2_inFlightFetches, "f").set(inFlightKey, fetchPromise);
|
|
149
|
+
await fetchPromise;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
_DeFiPositionsControllerV2_apiClient = new WeakMap(), _DeFiPositionsControllerV2_isEnabled = new WeakMap(), _DeFiPositionsControllerV2_getVsCurrency = new WeakMap(), _DeFiPositionsControllerV2_inFlightFetches = new WeakMap(), _DeFiPositionsControllerV2_instances = new WeakSet(), _DeFiPositionsControllerV2_fetchDeFiPositions = async function _DeFiPositionsControllerV2_fetchDeFiPositions(options, { networks, internalAccountIdByCaip }, vsCurrency) {
|
|
153
|
+
if (internalAccountIdByCaip.size === 0 || networks.length === 0) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const accountIds = [...internalAccountIdByCaip.keys()];
|
|
157
|
+
const queryOptions = {
|
|
158
|
+
networks,
|
|
159
|
+
includeDeFiBalances: true,
|
|
160
|
+
forceFetchDeFiPositions: true,
|
|
161
|
+
includePrices: true,
|
|
162
|
+
vsCurrency,
|
|
163
|
+
};
|
|
164
|
+
// Resolve once per fetch so a mid-poll remote-flag change cannot stretch
|
|
165
|
+
// or shrink this poll sequence inconsistently.
|
|
166
|
+
const { maxAttempts, pollInterval } = getProcessingPollConfig(this.messenger);
|
|
167
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
168
|
+
// First attempt respects forceRefresh; later polls always bypass cache
|
|
169
|
+
// so we do not spin on a stale processing snapshot.
|
|
170
|
+
const fetchOptions = {
|
|
171
|
+
...(options?.forceRefresh || attempt > 0 ? { staleTime: 0 } : {}),
|
|
172
|
+
};
|
|
173
|
+
let response;
|
|
92
174
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const positionsByAccount = groupDeFiPositionsV6({ ...response, accounts: readyAccounts }, internalAccountIdByCaip);
|
|
109
|
-
// Last valid response wins per ready account; processing / other accounts
|
|
110
|
-
// stay untouched.
|
|
175
|
+
response = await __classPrivateFieldGet(this, _DeFiPositionsControllerV2_apiClient, "f").accounts.fetchV6MultiAccountBalances(accountIds, queryOptions, fetchOptions);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// Soft-fail so prior state stays and the in-flight promise settles.
|
|
179
|
+
console.error('Failed to fetch DeFi positions', error);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
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.
|
|
186
|
+
if (!stillProcessing) {
|
|
187
|
+
const positionsByAccount = groupDeFiPositionsV6(response, internalAccountIdByCaip);
|
|
111
188
|
this.update((state) => {
|
|
112
189
|
for (const [accountId, positions] of Object.entries(positionsByAccount)) {
|
|
113
190
|
state.allDeFiPositionsV2[accountId] = positions;
|
|
114
191
|
}
|
|
115
192
|
});
|
|
193
|
+
// Report how many attempts were needed (only when polling was
|
|
194
|
+
// required) so Sentry can inform remote-flag / default poll-limit
|
|
195
|
+
// tuning without flooding on first-try successes.
|
|
196
|
+
const attemptsTaken = attempt + 1;
|
|
197
|
+
if (attemptsTaken > 1) {
|
|
198
|
+
const multipleAttemptsError = new Error(`DeFiPositionsControllerV2: positions ready after ${attemptsTaken} attempt(s)`);
|
|
199
|
+
multipleAttemptsError.name = 'DeFiPositionsV2FetchAttempts';
|
|
200
|
+
this.messenger.captureException?.(multipleAttemptsError);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
116
203
|
}
|
|
117
|
-
|
|
118
|
-
|
|
204
|
+
const { queryKey } = __classPrivateFieldGet(this, _DeFiPositionsControllerV2_apiClient, "f").accounts.getV6MultiAccountBalancesQueryOptions(accountIds, queryOptions, fetchOptions);
|
|
205
|
+
await __classPrivateFieldGet(this, _DeFiPositionsControllerV2_apiClient, "f").accounts.queryClient.invalidateQueries({
|
|
206
|
+
queryKey,
|
|
207
|
+
});
|
|
208
|
+
const isLastAttempt = attempt >= maxAttempts - 1;
|
|
209
|
+
if (isLastAttempt) {
|
|
210
|
+
// Report exhausted polls so Sentry can inform remote-flag / default
|
|
211
|
+
// poll-limit tuning when indexing never finishes in time.
|
|
212
|
+
const multipleAttemptsError = new Error(`DeFiPositionsControllerV2: still processing after ${maxAttempts} attempt(s)`);
|
|
213
|
+
multipleAttemptsError.name = 'DeFiPositionsV2ProcessingPollExhausted';
|
|
214
|
+
this.messenger.captureException?.(multipleAttemptsError);
|
|
215
|
+
return;
|
|
119
216
|
}
|
|
217
|
+
await delay(pollInterval);
|
|
120
218
|
}
|
|
121
|
-
}
|
|
122
|
-
_DeFiPositionsControllerV2_apiClient = new WeakMap(), _DeFiPositionsControllerV2_isEnabled = new WeakMap(), _DeFiPositionsControllerV2_getVsCurrency = new WeakMap();
|
|
219
|
+
};
|
|
123
220
|
//# sourceMappingURL=DeFiPositionsControllerV2.mjs.map
|
|
@@ -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';\n\nimport { buildDeFiBalancesQuery } from './build-defi-balances-query.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\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\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 for pull-to-refresh.\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 * @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. Each account key in\n * a ready response replaces that account's state (other accounts stay).\n * Accounts still indexing (`processingDefiPositions`) are skipped so prior\n * state is kept for them. 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 (e.g.\n * pull-to-refresh).\n *\n * @param options - Optional fetch modifiers.\n * @param options.forceRefresh - When true, bypass the apiClient cache and\n * fetch immediately.\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\n const { networks, internalAccountIdByCaip } =\n buildDeFiBalancesQuery(selectedAccounts);\n\n if (internalAccountIdByCaip.size === 0 || networks.length === 0) {\n return;\n }\n\n const accountIds = [...internalAccountIdByCaip.keys()];\n const vsCurrency = this.#getVsCurrency().toLowerCase();\n\n try {\n const response =\n await this.#apiClient.accounts.fetchV6MultiAccountBalances(\n accountIds,\n {\n networks,\n includeDeFiBalances: true,\n forceFetchDeFiPositions: true,\n includePrices: true,\n vsCurrency,\n },\n {\n // staleTime: 0 makes TanStack treat the cache as stale for this call.\n ...(options?.forceRefresh ? { staleTime: 0 } : {}),\n },\n );\n\n // Skip accounts still indexing — their balances are not a valid snapshot.\n const readyAccounts = response.accounts.filter(\n (account) => !account.processingDefiPositions,\n );\n if (readyAccounts.length === 0) {\n return;\n }\n\n const positionsByAccount = groupDeFiPositionsV6(\n { ...response, accounts: readyAccounts },\n internalAccountIdByCaip,\n );\n\n // Last valid response wins per ready account; processing / other accounts\n // stay untouched.\n this.update((state) => {\n for (const [accountId, positions] of Object.entries(\n positionsByAccount,\n )) {\n state.allDeFiPositionsV2[accountId] = positions;\n }\n });\n } catch (error) {\n console.error('Failed to fetch DeFi positions', error);\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"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getProcessingPollConfig = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Remote feature flag key for DeFi Positions Controller V2 (camelCase, as
|
|
6
|
+
* stored by RemoteFeatureFlagController after client-config resolution).
|
|
7
|
+
*/
|
|
8
|
+
const DEFI_CONTROLLER_V2_FEATURE_FLAG = 'defiControllerV2';
|
|
9
|
+
/** Delay between polls while Accounts API reports DeFi indexing in progress. */
|
|
10
|
+
const DEFAULT_PROCESSING_POLL_INTERVAL_MS = 5000;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum fetch attempts (including the first) while any account still has
|
|
13
|
+
* `processingDefiPositions: true`. After this, the call resolves without
|
|
14
|
+
* updating state, so prior positions are kept for every selected account.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS = 5;
|
|
17
|
+
/**
|
|
18
|
+
* @param config - Optional remote poll overrides.
|
|
19
|
+
* @returns Resolved positive integer max attempts and poll interval ms.
|
|
20
|
+
*/
|
|
21
|
+
function resolveProcessingPollConfig(config) {
|
|
22
|
+
const maxAttempts = typeof config?.maxAttempts === 'number' &&
|
|
23
|
+
Number.isFinite(config.maxAttempts) &&
|
|
24
|
+
config.maxAttempts > 0
|
|
25
|
+
? Math.floor(config.maxAttempts)
|
|
26
|
+
: DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS;
|
|
27
|
+
const pollInterval = typeof config?.pollInterval === 'number' &&
|
|
28
|
+
Number.isFinite(config.pollInterval) &&
|
|
29
|
+
config.pollInterval > 0
|
|
30
|
+
? config.pollInterval
|
|
31
|
+
: DEFAULT_PROCESSING_POLL_INTERVAL_MS;
|
|
32
|
+
return { maxAttempts, pollInterval };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Narrows a remote feature-flag JSON value to the DeFi V2 poll config fields.
|
|
36
|
+
*
|
|
37
|
+
* @param flag - Raw flag value from RemoteFeatureFlagController state.
|
|
38
|
+
* @returns Poll config fields when present, otherwise `undefined`.
|
|
39
|
+
*/
|
|
40
|
+
function parseDeFiControllerV2FeatureFlag(flag) {
|
|
41
|
+
if (!flag || typeof flag !== 'object' || Array.isArray(flag)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const { maxAttempts, pollInterval } = flag;
|
|
45
|
+
return { maxAttempts, pollInterval };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reads `defiControllerV2` from RemoteFeatureFlagController and returns
|
|
49
|
+
* resolved processing-poll limits. Missing or invalid flag values fall back to
|
|
50
|
+
* {@link DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS} /
|
|
51
|
+
* {@link DEFAULT_PROCESSING_POLL_INTERVAL_MS}.
|
|
52
|
+
*
|
|
53
|
+
* @param messenger - Messenger that can call
|
|
54
|
+
* `RemoteFeatureFlagController:getState`.
|
|
55
|
+
* @returns Positive integer max attempts and poll interval ms.
|
|
56
|
+
*/
|
|
57
|
+
function getProcessingPollConfig(messenger) {
|
|
58
|
+
const { remoteFeatureFlags } = messenger.call('RemoteFeatureFlagController:getState');
|
|
59
|
+
return resolveProcessingPollConfig(parseDeFiControllerV2FeatureFlag(remoteFeatureFlags?.[DEFI_CONTROLLER_V2_FEATURE_FLAG]));
|
|
60
|
+
}
|
|
61
|
+
exports.getProcessingPollConfig = getProcessingPollConfig;
|
|
62
|
+
//# sourceMappingURL=defi-controller-v2-feature-flag.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defi-controller-v2-feature-flag.cjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/defi-controller-v2-feature-flag.ts"],"names":[],"mappings":";;;AAGA;;;GAGG;AACH,MAAM,+BAA+B,GAAG,kBAAkB,CAAC;AAE3D,gFAAgF;AAChF,MAAM,mCAAmC,GAAG,IAAK,CAAC;AAElD;;;;GAIG;AACH,MAAM,oCAAoC,GAAG,CAAC,CAAC;AAyC/C;;;GAGG;AACH,SAAS,2BAA2B,CAClC,MAA6D;IAE7D,MAAM,WAAW,GACf,OAAO,MAAM,EAAE,WAAW,KAAK,QAAQ;QACvC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC;QACnC,MAAM,CAAC,WAAW,GAAG,CAAC;QACpB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC;QAChC,CAAC,CAAC,oCAAoC,CAAC;IAC3C,MAAM,YAAY,GAChB,OAAO,MAAM,EAAE,YAAY,KAAK,QAAQ;QACxC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC;QACpC,MAAM,CAAC,YAAY,GAAG,CAAC;QACrB,CAAC,CAAC,MAAM,CAAC,YAAY;QACrB,CAAC,CAAC,mCAAmC,CAAC;IAE1C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AACvC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gCAAgC,CACvC,IAAsB;IAEtB,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7D,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,IAAmC,CAAC;IAC1E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AACvC,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,uBAAuB,CACrC,SAA2C;IAE3C,MAAM,EAAE,kBAAkB,EAAE,GAAG,SAAS,CAAC,IAAI,CAC3C,sCAAsC,CACvC,CAAC;IACF,OAAO,2BAA2B,CAChC,gCAAgC,CAC9B,kBAAkB,EAAE,CAAC,+BAA+B,CAAC,CACtD,CACF,CAAC;AACJ,CAAC;AAXD,0DAWC","sourcesContent":["import type { RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag-controller';\nimport type { Json } from '@metamask/utils';\n\n/**\n * Remote feature flag key for DeFi Positions Controller V2 (camelCase, as\n * stored by RemoteFeatureFlagController after client-config resolution).\n */\nconst DEFI_CONTROLLER_V2_FEATURE_FLAG = 'defiControllerV2';\n\n/** Delay between polls while Accounts API reports DeFi indexing in progress. */\nconst DEFAULT_PROCESSING_POLL_INTERVAL_MS = 5_000;\n\n/**\n * Maximum fetch attempts (including the first) while any account still has\n * `processingDefiPositions: true`. After this, the call resolves without\n * updating state, so prior positions are kept for every selected account.\n */\nconst DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS = 5;\n\n/**\n * Resolved `defiControllerV2` remote feature flag shape used for processing\n * poll overrides. `enabled` is read by clients for gating; the controller\n * only consumes `maxAttempts` / `pollInterval`.\n */\ntype DeFiControllerV2FeatureFlag = {\n enabled?: boolean;\n maxAttempts?: number;\n pollInterval?: number;\n};\n\n/**\n * Optional processing-poll overrides from {@link DeFiControllerV2FeatureFlag}.\n * Missing or non-positive values fall back to\n * {@link DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS} /\n * {@link DEFAULT_PROCESSING_POLL_INTERVAL_MS}.\n */\ntype DeFiPositionsControllerV2ProcessingPollConfig = {\n maxAttempts?: number;\n pollInterval?: number;\n};\n\n/**\n * Resolved positive integer processing-poll limits.\n */\ntype ResolvedProcessingPollConfig = {\n maxAttempts: number;\n pollInterval: number;\n};\n\n/**\n * Messenger surface needed to read the DeFi V2 remote feature flag.\n */\ntype GetProcessingPollConfigMessenger = {\n call: (\n actionType: 'RemoteFeatureFlagController:getState',\n ) => RemoteFeatureFlagControllerState;\n};\n\n/**\n * @param config - Optional remote poll overrides.\n * @returns Resolved positive integer max attempts and poll interval ms.\n */\nfunction resolveProcessingPollConfig(\n config?: DeFiPositionsControllerV2ProcessingPollConfig | null,\n): ResolvedProcessingPollConfig {\n const maxAttempts =\n typeof config?.maxAttempts === 'number' &&\n Number.isFinite(config.maxAttempts) &&\n config.maxAttempts > 0\n ? Math.floor(config.maxAttempts)\n : DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS;\n const pollInterval =\n typeof config?.pollInterval === 'number' &&\n Number.isFinite(config.pollInterval) &&\n config.pollInterval > 0\n ? config.pollInterval\n : DEFAULT_PROCESSING_POLL_INTERVAL_MS;\n\n return { maxAttempts, pollInterval };\n}\n\n/**\n * Narrows a remote feature-flag JSON value to the DeFi V2 poll config fields.\n *\n * @param flag - Raw flag value from RemoteFeatureFlagController state.\n * @returns Poll config fields when present, otherwise `undefined`.\n */\nfunction parseDeFiControllerV2FeatureFlag(\n flag: Json | undefined,\n): DeFiPositionsControllerV2ProcessingPollConfig | undefined {\n if (!flag || typeof flag !== 'object' || Array.isArray(flag)) {\n return undefined;\n }\n\n const { maxAttempts, pollInterval } = flag as DeFiControllerV2FeatureFlag;\n return { maxAttempts, pollInterval };\n}\n\n/**\n * Reads `defiControllerV2` from RemoteFeatureFlagController and returns\n * resolved processing-poll limits. Missing or invalid flag values fall back to\n * {@link DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS} /\n * {@link DEFAULT_PROCESSING_POLL_INTERVAL_MS}.\n *\n * @param messenger - Messenger that can call\n * `RemoteFeatureFlagController:getState`.\n * @returns Positive integer max attempts and poll interval ms.\n */\nexport function getProcessingPollConfig(\n messenger: GetProcessingPollConfigMessenger,\n): ResolvedProcessingPollConfig {\n const { remoteFeatureFlags } = messenger.call(\n 'RemoteFeatureFlagController:getState',\n );\n return resolveProcessingPollConfig(\n parseDeFiControllerV2FeatureFlag(\n remoteFeatureFlags?.[DEFI_CONTROLLER_V2_FEATURE_FLAG],\n ),\n );\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RemoteFeatureFlagControllerState } from "@metamask/remote-feature-flag-controller";
|
|
2
|
+
/**
|
|
3
|
+
* Resolved positive integer processing-poll limits.
|
|
4
|
+
*/
|
|
5
|
+
type ResolvedProcessingPollConfig = {
|
|
6
|
+
maxAttempts: number;
|
|
7
|
+
pollInterval: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Messenger surface needed to read the DeFi V2 remote feature flag.
|
|
11
|
+
*/
|
|
12
|
+
type GetProcessingPollConfigMessenger = {
|
|
13
|
+
call: (actionType: 'RemoteFeatureFlagController:getState') => RemoteFeatureFlagControllerState;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Reads `defiControllerV2` from RemoteFeatureFlagController and returns
|
|
17
|
+
* resolved processing-poll limits. Missing or invalid flag values fall back to
|
|
18
|
+
* {@link DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS} /
|
|
19
|
+
* {@link DEFAULT_PROCESSING_POLL_INTERVAL_MS}.
|
|
20
|
+
*
|
|
21
|
+
* @param messenger - Messenger that can call
|
|
22
|
+
* `RemoteFeatureFlagController:getState`.
|
|
23
|
+
* @returns Positive integer max attempts and poll interval ms.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getProcessingPollConfig(messenger: GetProcessingPollConfigMessenger): ResolvedProcessingPollConfig;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=defi-controller-v2-feature-flag.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defi-controller-v2-feature-flag.d.cts","sourceRoot":"","sources":["../../src/DeFiPositionsController/defi-controller-v2-feature-flag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gCAAgC,EAAE,iDAAiD;AAyCjG;;GAEG;AACH,KAAK,4BAA4B,GAAG;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,KAAK,gCAAgC,GAAG;IACtC,IAAI,EAAE,CACJ,UAAU,EAAE,sCAAsC,KAC/C,gCAAgC,CAAC;CACvC,CAAC;AA0CF;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,gCAAgC,GAC1C,4BAA4B,CAS9B"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RemoteFeatureFlagControllerState } from "@metamask/remote-feature-flag-controller";
|
|
2
|
+
/**
|
|
3
|
+
* Resolved positive integer processing-poll limits.
|
|
4
|
+
*/
|
|
5
|
+
type ResolvedProcessingPollConfig = {
|
|
6
|
+
maxAttempts: number;
|
|
7
|
+
pollInterval: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Messenger surface needed to read the DeFi V2 remote feature flag.
|
|
11
|
+
*/
|
|
12
|
+
type GetProcessingPollConfigMessenger = {
|
|
13
|
+
call: (actionType: 'RemoteFeatureFlagController:getState') => RemoteFeatureFlagControllerState;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Reads `defiControllerV2` from RemoteFeatureFlagController and returns
|
|
17
|
+
* resolved processing-poll limits. Missing or invalid flag values fall back to
|
|
18
|
+
* {@link DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS} /
|
|
19
|
+
* {@link DEFAULT_PROCESSING_POLL_INTERVAL_MS}.
|
|
20
|
+
*
|
|
21
|
+
* @param messenger - Messenger that can call
|
|
22
|
+
* `RemoteFeatureFlagController:getState`.
|
|
23
|
+
* @returns Positive integer max attempts and poll interval ms.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getProcessingPollConfig(messenger: GetProcessingPollConfigMessenger): ResolvedProcessingPollConfig;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=defi-controller-v2-feature-flag.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defi-controller-v2-feature-flag.d.mts","sourceRoot":"","sources":["../../src/DeFiPositionsController/defi-controller-v2-feature-flag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gCAAgC,EAAE,iDAAiD;AAyCjG;;GAEG;AACH,KAAK,4BAA4B,GAAG;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,KAAK,gCAAgC,GAAG;IACtC,IAAI,EAAE,CACJ,UAAU,EAAE,sCAAsC,KAC/C,gCAAgC,CAAC;CACvC,CAAC;AA0CF;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,gCAAgC,GAC1C,4BAA4B,CAS9B"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote feature flag key for DeFi Positions Controller V2 (camelCase, as
|
|
3
|
+
* stored by RemoteFeatureFlagController after client-config resolution).
|
|
4
|
+
*/
|
|
5
|
+
const DEFI_CONTROLLER_V2_FEATURE_FLAG = 'defiControllerV2';
|
|
6
|
+
/** Delay between polls while Accounts API reports DeFi indexing in progress. */
|
|
7
|
+
const DEFAULT_PROCESSING_POLL_INTERVAL_MS = 5000;
|
|
8
|
+
/**
|
|
9
|
+
* Maximum fetch attempts (including the first) while any account still has
|
|
10
|
+
* `processingDefiPositions: true`. After this, the call resolves without
|
|
11
|
+
* updating state, so prior positions are kept for every selected account.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS = 5;
|
|
14
|
+
/**
|
|
15
|
+
* @param config - Optional remote poll overrides.
|
|
16
|
+
* @returns Resolved positive integer max attempts and poll interval ms.
|
|
17
|
+
*/
|
|
18
|
+
function resolveProcessingPollConfig(config) {
|
|
19
|
+
const maxAttempts = typeof config?.maxAttempts === 'number' &&
|
|
20
|
+
Number.isFinite(config.maxAttempts) &&
|
|
21
|
+
config.maxAttempts > 0
|
|
22
|
+
? Math.floor(config.maxAttempts)
|
|
23
|
+
: DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS;
|
|
24
|
+
const pollInterval = typeof config?.pollInterval === 'number' &&
|
|
25
|
+
Number.isFinite(config.pollInterval) &&
|
|
26
|
+
config.pollInterval > 0
|
|
27
|
+
? config.pollInterval
|
|
28
|
+
: DEFAULT_PROCESSING_POLL_INTERVAL_MS;
|
|
29
|
+
return { maxAttempts, pollInterval };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Narrows a remote feature-flag JSON value to the DeFi V2 poll config fields.
|
|
33
|
+
*
|
|
34
|
+
* @param flag - Raw flag value from RemoteFeatureFlagController state.
|
|
35
|
+
* @returns Poll config fields when present, otherwise `undefined`.
|
|
36
|
+
*/
|
|
37
|
+
function parseDeFiControllerV2FeatureFlag(flag) {
|
|
38
|
+
if (!flag || typeof flag !== 'object' || Array.isArray(flag)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const { maxAttempts, pollInterval } = flag;
|
|
42
|
+
return { maxAttempts, pollInterval };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reads `defiControllerV2` from RemoteFeatureFlagController and returns
|
|
46
|
+
* resolved processing-poll limits. Missing or invalid flag values fall back to
|
|
47
|
+
* {@link DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS} /
|
|
48
|
+
* {@link DEFAULT_PROCESSING_POLL_INTERVAL_MS}.
|
|
49
|
+
*
|
|
50
|
+
* @param messenger - Messenger that can call
|
|
51
|
+
* `RemoteFeatureFlagController:getState`.
|
|
52
|
+
* @returns Positive integer max attempts and poll interval ms.
|
|
53
|
+
*/
|
|
54
|
+
export function getProcessingPollConfig(messenger) {
|
|
55
|
+
const { remoteFeatureFlags } = messenger.call('RemoteFeatureFlagController:getState');
|
|
56
|
+
return resolveProcessingPollConfig(parseDeFiControllerV2FeatureFlag(remoteFeatureFlags?.[DEFI_CONTROLLER_V2_FEATURE_FLAG]));
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=defi-controller-v2-feature-flag.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defi-controller-v2-feature-flag.mjs","sourceRoot":"","sources":["../../src/DeFiPositionsController/defi-controller-v2-feature-flag.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,+BAA+B,GAAG,kBAAkB,CAAC;AAE3D,gFAAgF;AAChF,MAAM,mCAAmC,GAAG,IAAK,CAAC;AAElD;;;;GAIG;AACH,MAAM,oCAAoC,GAAG,CAAC,CAAC;AAyC/C;;;GAGG;AACH,SAAS,2BAA2B,CAClC,MAA6D;IAE7D,MAAM,WAAW,GACf,OAAO,MAAM,EAAE,WAAW,KAAK,QAAQ;QACvC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC;QACnC,MAAM,CAAC,WAAW,GAAG,CAAC;QACpB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC;QAChC,CAAC,CAAC,oCAAoC,CAAC;IAC3C,MAAM,YAAY,GAChB,OAAO,MAAM,EAAE,YAAY,KAAK,QAAQ;QACxC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC;QACpC,MAAM,CAAC,YAAY,GAAG,CAAC;QACrB,CAAC,CAAC,MAAM,CAAC,YAAY;QACrB,CAAC,CAAC,mCAAmC,CAAC;IAE1C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AACvC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gCAAgC,CACvC,IAAsB;IAEtB,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7D,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,IAAmC,CAAC;IAC1E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC;AACvC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,uBAAuB,CACrC,SAA2C;IAE3C,MAAM,EAAE,kBAAkB,EAAE,GAAG,SAAS,CAAC,IAAI,CAC3C,sCAAsC,CACvC,CAAC;IACF,OAAO,2BAA2B,CAChC,gCAAgC,CAC9B,kBAAkB,EAAE,CAAC,+BAA+B,CAAC,CACtD,CACF,CAAC;AACJ,CAAC","sourcesContent":["import type { RemoteFeatureFlagControllerState } from '@metamask/remote-feature-flag-controller';\nimport type { Json } from '@metamask/utils';\n\n/**\n * Remote feature flag key for DeFi Positions Controller V2 (camelCase, as\n * stored by RemoteFeatureFlagController after client-config resolution).\n */\nconst DEFI_CONTROLLER_V2_FEATURE_FLAG = 'defiControllerV2';\n\n/** Delay between polls while Accounts API reports DeFi indexing in progress. */\nconst DEFAULT_PROCESSING_POLL_INTERVAL_MS = 5_000;\n\n/**\n * Maximum fetch attempts (including the first) while any account still has\n * `processingDefiPositions: true`. After this, the call resolves without\n * updating state, so prior positions are kept for every selected account.\n */\nconst DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS = 5;\n\n/**\n * Resolved `defiControllerV2` remote feature flag shape used for processing\n * poll overrides. `enabled` is read by clients for gating; the controller\n * only consumes `maxAttempts` / `pollInterval`.\n */\ntype DeFiControllerV2FeatureFlag = {\n enabled?: boolean;\n maxAttempts?: number;\n pollInterval?: number;\n};\n\n/**\n * Optional processing-poll overrides from {@link DeFiControllerV2FeatureFlag}.\n * Missing or non-positive values fall back to\n * {@link DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS} /\n * {@link DEFAULT_PROCESSING_POLL_INTERVAL_MS}.\n */\ntype DeFiPositionsControllerV2ProcessingPollConfig = {\n maxAttempts?: number;\n pollInterval?: number;\n};\n\n/**\n * Resolved positive integer processing-poll limits.\n */\ntype ResolvedProcessingPollConfig = {\n maxAttempts: number;\n pollInterval: number;\n};\n\n/**\n * Messenger surface needed to read the DeFi V2 remote feature flag.\n */\ntype GetProcessingPollConfigMessenger = {\n call: (\n actionType: 'RemoteFeatureFlagController:getState',\n ) => RemoteFeatureFlagControllerState;\n};\n\n/**\n * @param config - Optional remote poll overrides.\n * @returns Resolved positive integer max attempts and poll interval ms.\n */\nfunction resolveProcessingPollConfig(\n config?: DeFiPositionsControllerV2ProcessingPollConfig | null,\n): ResolvedProcessingPollConfig {\n const maxAttempts =\n typeof config?.maxAttempts === 'number' &&\n Number.isFinite(config.maxAttempts) &&\n config.maxAttempts > 0\n ? Math.floor(config.maxAttempts)\n : DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS;\n const pollInterval =\n typeof config?.pollInterval === 'number' &&\n Number.isFinite(config.pollInterval) &&\n config.pollInterval > 0\n ? config.pollInterval\n : DEFAULT_PROCESSING_POLL_INTERVAL_MS;\n\n return { maxAttempts, pollInterval };\n}\n\n/**\n * Narrows a remote feature-flag JSON value to the DeFi V2 poll config fields.\n *\n * @param flag - Raw flag value from RemoteFeatureFlagController state.\n * @returns Poll config fields when present, otherwise `undefined`.\n */\nfunction parseDeFiControllerV2FeatureFlag(\n flag: Json | undefined,\n): DeFiPositionsControllerV2ProcessingPollConfig | undefined {\n if (!flag || typeof flag !== 'object' || Array.isArray(flag)) {\n return undefined;\n }\n\n const { maxAttempts, pollInterval } = flag as DeFiControllerV2FeatureFlag;\n return { maxAttempts, pollInterval };\n}\n\n/**\n * Reads `defiControllerV2` from RemoteFeatureFlagController and returns\n * resolved processing-poll limits. Missing or invalid flag values fall back to\n * {@link DEFAULT_PROCESSING_POLL_MAX_ATTEMPTS} /\n * {@link DEFAULT_PROCESSING_POLL_INTERVAL_MS}.\n *\n * @param messenger - Messenger that can call\n * `RemoteFeatureFlagController:getState`.\n * @returns Positive integer max attempts and poll interval ms.\n */\nexport function getProcessingPollConfig(\n messenger: GetProcessingPollConfigMessenger,\n): ResolvedProcessingPollConfig {\n const { remoteFeatureFlags } = messenger.call(\n 'RemoteFeatureFlagController:getState',\n );\n return resolveProcessingPollConfig(\n parseDeFiControllerV2FeatureFlag(\n remoteFeatureFlags?.[DEFI_CONTROLLER_V2_FEATURE_FLAG],\n ),\n );\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metamask-previews/assets-controllers",
|
|
3
|
-
"version": "110.1.
|
|
3
|
+
"version": "110.1.1-preview-abbcfa61d",
|
|
4
4
|
"description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Ethereum",
|
|
@@ -62,13 +62,13 @@
|
|
|
62
62
|
"@ethersproject/contracts": "^5.7.0",
|
|
63
63
|
"@ethersproject/providers": "^5.7.0",
|
|
64
64
|
"@metamask/abi-utils": "^2.0.3",
|
|
65
|
-
"@metamask/account-tree-controller": "^7.
|
|
65
|
+
"@metamask/account-tree-controller": "^7.6.0",
|
|
66
66
|
"@metamask/accounts-controller": "^39.0.6",
|
|
67
67
|
"@metamask/approval-controller": "^9.0.2",
|
|
68
68
|
"@metamask/base-controller": "^9.1.0",
|
|
69
69
|
"@metamask/contract-metadata": "^2.4.0",
|
|
70
70
|
"@metamask/controller-utils": "^12.3.0",
|
|
71
|
-
"@metamask/core-backend": "^8.1.
|
|
71
|
+
"@metamask/core-backend": "^8.1.1",
|
|
72
72
|
"@metamask/eth-query": "^4.0.0",
|
|
73
73
|
"@metamask/keyring-api": "^23.7.0",
|
|
74
74
|
"@metamask/keyring-controller": "^27.1.0",
|
|
@@ -81,13 +81,14 @@
|
|
|
81
81
|
"@metamask/phishing-controller": "^17.3.1",
|
|
82
82
|
"@metamask/polling-controller": "^16.0.9",
|
|
83
83
|
"@metamask/preferences-controller": "^23.1.0",
|
|
84
|
-
"@metamask/profile-sync-controller": "^
|
|
84
|
+
"@metamask/profile-sync-controller": "^29.0.0",
|
|
85
|
+
"@metamask/remote-feature-flag-controller": "^5.0.0",
|
|
85
86
|
"@metamask/rpc-errors": "^7.0.2",
|
|
86
87
|
"@metamask/snaps-controllers": "^19.0.0",
|
|
87
88
|
"@metamask/snaps-sdk": "^11.0.0",
|
|
88
89
|
"@metamask/snaps-utils": "^12.1.2",
|
|
89
90
|
"@metamask/storage-service": "^1.0.2",
|
|
90
|
-
"@metamask/transaction-controller": "^69.
|
|
91
|
+
"@metamask/transaction-controller": "^69.5.0",
|
|
91
92
|
"@metamask/utils": "^11.11.0",
|
|
92
93
|
"@tanstack/query-core": "^5.62.16",
|
|
93
94
|
"@types/bn.js": "^5.1.5",
|