@metamask/assets-controller 9.1.0 → 10.0.0

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/AssetsController.cjs +351 -90
  3. package/dist/AssetsController.cjs.map +1 -1
  4. package/dist/AssetsController.d.cts +11 -8
  5. package/dist/AssetsController.d.cts.map +1 -1
  6. package/dist/AssetsController.d.mts +11 -8
  7. package/dist/AssetsController.d.mts.map +1 -1
  8. package/dist/AssetsController.mjs +351 -90
  9. package/dist/AssetsController.mjs.map +1 -1
  10. package/dist/data-sources/AccountsApiDataSource.cjs +14 -1
  11. package/dist/data-sources/AccountsApiDataSource.cjs.map +1 -1
  12. package/dist/data-sources/AccountsApiDataSource.d.cts +8 -0
  13. package/dist/data-sources/AccountsApiDataSource.d.cts.map +1 -1
  14. package/dist/data-sources/AccountsApiDataSource.d.mts +8 -0
  15. package/dist/data-sources/AccountsApiDataSource.d.mts.map +1 -1
  16. package/dist/data-sources/AccountsApiDataSource.mjs +14 -1
  17. package/dist/data-sources/AccountsApiDataSource.mjs.map +1 -1
  18. package/dist/data-sources/BackendWebsocketDataSource.cjs +236 -149
  19. package/dist/data-sources/BackendWebsocketDataSource.cjs.map +1 -1
  20. package/dist/data-sources/BackendWebsocketDataSource.d.cts +15 -0
  21. package/dist/data-sources/BackendWebsocketDataSource.d.cts.map +1 -1
  22. package/dist/data-sources/BackendWebsocketDataSource.d.mts +15 -0
  23. package/dist/data-sources/BackendWebsocketDataSource.d.mts.map +1 -1
  24. package/dist/data-sources/BackendWebsocketDataSource.mjs +236 -146
  25. package/dist/data-sources/BackendWebsocketDataSource.mjs.map +1 -1
  26. package/dist/data-sources/PriceDataSource.cjs +18 -9
  27. package/dist/data-sources/PriceDataSource.cjs.map +1 -1
  28. package/dist/data-sources/PriceDataSource.d.cts.map +1 -1
  29. package/dist/data-sources/PriceDataSource.d.mts.map +1 -1
  30. package/dist/data-sources/PriceDataSource.mjs +19 -10
  31. package/dist/data-sources/PriceDataSource.mjs.map +1 -1
  32. package/dist/data-sources/RpcDataSource.cjs +29 -9
  33. package/dist/data-sources/RpcDataSource.cjs.map +1 -1
  34. package/dist/data-sources/RpcDataSource.d.cts +5 -0
  35. package/dist/data-sources/RpcDataSource.d.cts.map +1 -1
  36. package/dist/data-sources/RpcDataSource.d.mts +5 -0
  37. package/dist/data-sources/RpcDataSource.d.mts.map +1 -1
  38. package/dist/data-sources/RpcDataSource.mjs +29 -9
  39. package/dist/data-sources/RpcDataSource.mjs.map +1 -1
  40. package/dist/middlewares/DetectionMiddleware.cjs +22 -1
  41. package/dist/middlewares/DetectionMiddleware.cjs.map +1 -1
  42. package/dist/middlewares/DetectionMiddleware.d.cts +3 -0
  43. package/dist/middlewares/DetectionMiddleware.d.cts.map +1 -1
  44. package/dist/middlewares/DetectionMiddleware.d.mts +3 -0
  45. package/dist/middlewares/DetectionMiddleware.d.mts.map +1 -1
  46. package/dist/middlewares/DetectionMiddleware.mjs +22 -1
  47. package/dist/middlewares/DetectionMiddleware.mjs.map +1 -1
  48. package/dist/middlewares/ParallelMiddleware.cjs +7 -0
  49. package/dist/middlewares/ParallelMiddleware.cjs.map +1 -1
  50. package/dist/middlewares/ParallelMiddleware.d.cts.map +1 -1
  51. package/dist/middlewares/ParallelMiddleware.d.mts.map +1 -1
  52. package/dist/middlewares/ParallelMiddleware.mjs +7 -0
  53. package/dist/middlewares/ParallelMiddleware.mjs.map +1 -1
  54. package/dist/types.cjs.map +1 -1
  55. package/dist/types.d.cts +20 -4
  56. package/dist/types.d.cts.map +1 -1
  57. package/dist/types.d.mts +20 -4
  58. package/dist/types.d.mts.map +1 -1
  59. package/dist/types.mjs.map +1 -1
  60. package/dist/utils/dedupingBatchFetcher.cjs +11 -0
  61. package/dist/utils/dedupingBatchFetcher.cjs.map +1 -1
  62. package/dist/utils/dedupingBatchFetcher.d.cts +7 -0
  63. package/dist/utils/dedupingBatchFetcher.d.cts.map +1 -1
  64. package/dist/utils/dedupingBatchFetcher.d.mts +7 -0
  65. package/dist/utils/dedupingBatchFetcher.d.mts.map +1 -1
  66. package/dist/utils/dedupingBatchFetcher.mjs +11 -0
  67. package/dist/utils/dedupingBatchFetcher.mjs.map +1 -1
  68. package/dist/utils/processAccountActivityBalanceUpdates.cjs +54 -0
  69. package/dist/utils/processAccountActivityBalanceUpdates.cjs.map +1 -0
  70. package/dist/utils/processAccountActivityBalanceUpdates.d.cts +13 -0
  71. package/dist/utils/processAccountActivityBalanceUpdates.d.cts.map +1 -0
  72. package/dist/utils/processAccountActivityBalanceUpdates.d.mts +13 -0
  73. package/dist/utils/processAccountActivityBalanceUpdates.d.mts.map +1 -0
  74. package/dist/utils/processAccountActivityBalanceUpdates.mjs +47 -0
  75. package/dist/utils/processAccountActivityBalanceUpdates.mjs.map +1 -0
  76. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"types.mjs","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAyfA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CAC1B,SAAqB,EACrB,UAAsB;IAEtB,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,MAAM,cAAc,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC;QAC7C,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;QAEtE,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,OAAO,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type { InternalAccount } from '@metamask/keyring-internal-api';\nimport type { CaipAssetType, CaipChainId, Json } from '@metamask/utils';\n\n/**\n * CAIP-19 compliant asset identifier\n * Format: \"{chainId}/{assetNamespace}:{assetReference}[/tokenId]\"\n *\n * Examples:\n * - Native: \"eip155:1/slip44:60\" (ETH)\n * - ERC20: \"eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48\" (USDC)\n * - ERC721: \"eip155:1/erc721:0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D/1234\" (BAYC #1234)\n * - SPL: \"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\"\n */\nexport type Caip19AssetId = CaipAssetType;\n\n/**\n * InternalAccount UUID from AccountsController\n * Not the blockchain address!\n */\nexport type AccountId = string;\n\n/**\n * CAIP-2 chain identifier\n */\nexport type ChainId = CaipChainId;\n\n// ============================================================================\n// ASSET TYPES - Defined by metadata structure\n// ============================================================================\n\n/**\n * Asset types define the metadata structure, not blockchain implementation.\n * - \"fungible\" includes: native, erc20, spl - all share balance, symbol, decimals\n * - \"nft\" includes: erc721, erc1155 - include tokenId, image, attributes\n */\nexport type AssetType = 'fungible' | 'nft' | 'collectible';\n\n/**\n * Token standards - blockchain implementation details\n */\nexport type TokenStandard =\n | 'native'\n | 'erc20'\n | 'erc721'\n | 'erc1155'\n | 'spl'\n | string;\n\n// ============================================================================\n// METADATA TYPES (vary by asset type)\n// ============================================================================\n\n/**\n * UI preferences for an asset (stored in assetPreferences state, not in metadata).\n */\nexport type AssetPreferences = {\n /** Whether the asset is hidden from display */\n hidden?: boolean;\n};\n\n/**\n * Base metadata attributes shared by ALL asset types.\n */\nexport type BaseAssetMetadata = {\n /** Token standard - how it's implemented on the blockchain */\n type: TokenStandard;\n /** Display symbol (e.g., \"ETH\", \"USDC\") */\n symbol: string;\n /** Full name (e.g., \"Ethereum\", \"USD Coin\") */\n name: string;\n /** Token decimals (18 for ETH, 6 for USDC, etc.) */\n decimals: number;\n /** Logo URL or data URI */\n image?: string;\n};\n\n// ============================================================================\n// TOKEN CONTRACT DATA TYPES\n// ============================================================================\n\n/** Fee information for token transfers */\nexport type TokenFees = {\n avgFee: number;\n maxFee: number;\n minFee: number;\n};\n\n/** Honeypot detection status */\nexport type HoneypotStatus = {\n honeypotIs: boolean;\n goPlus?: boolean;\n};\n\n/** Storage slot information for the contract */\nexport type StorageSlots = {\n balance: number;\n approval: number;\n};\n\n/** Localized description */\nexport type LocalizedDescription = {\n en: string;\n};\n\n// ============================================================================\n// ASSET METADATA TYPES\n// ============================================================================\n\n/**\n * Metadata for fungible tokens.\n * Structure mirrors V3AssetResponse from the Tokens API.\n *\n * Differences from V3AssetResponse:\n * - `type` is derived from assetId namespace (not in API response)\n * - `image` maps from API's `iconUrl`\n * - `assetId` is not stored (used as the key)\n */\nexport type FungibleAssetMetadata = {\n /** Token type derived from assetId namespace */\n type: 'native' | 'erc20' | 'spl';\n /** CoinGecko ID for price lookups */\n coingeckoId?: string;\n /** Number of token list occurrences */\n occurrences?: number;\n /** DEX/aggregator integrations */\n aggregators?: string[];\n /** Asset labels/tags (e.g., \"stable_coin\") */\n labels?: string[];\n /** Whether the token supports ERC-20 permit */\n erc20Permit?: boolean;\n /** Fee information for token transfers */\n fees?: TokenFees;\n /** Honeypot detection status */\n honeypotStatus?: HoneypotStatus;\n /** Storage slot information for the contract */\n storage?: StorageSlots;\n /** Whether the contract is verified */\n isContractVerified?: boolean;\n /** Localized description */\n description?: LocalizedDescription;\n} & BaseAssetMetadata;\n\n/**\n * Metadata for ERC721 NFTs\n * Asset Type: \"nft\"\n */\nexport type ERC721AssetMetadata = {\n type: 'erc721';\n decimals: 0;\n /** Collection name */\n collectionName?: string;\n /** Collection size */\n collectionSize?: number;\n /** NFT traits/attributes - must be Json-serializable */\n traits?: Record<string, Json>;\n /** Rarity score */\n rarity?: number;\n /** Verification status */\n verified?: boolean;\n} & BaseAssetMetadata;\n\n/**\n * Metadata for ERC1155 multi-tokens\n */\nexport type ERC1155AssetMetadata = {\n type: 'erc1155';\n /** Token URI */\n tokenUri?: string;\n /** Token category */\n category?: string;\n /** Spam detection flag */\n isSpam?: boolean;\n} & BaseAssetMetadata;\n\n/**\n * Union type representing all possible asset metadata types.\n * All types must be JSON-serializable.\n */\nexport type AssetMetadata =\n | FungibleAssetMetadata\n | ERC721AssetMetadata\n | ERC1155AssetMetadata\n | (BaseAssetMetadata & { [key: string]: Json });\n\n// ============================================================================\n// PRICE TYPES (vary by asset type)\n// ============================================================================\n\n/**\n * Base price attributes.\n */\nexport type BaseAssetPrice = {\n /** Current price in selected currency */\n price: number;\n /** Timestamp of last price update */\n lastUpdated: number;\n};\n\n/**\n * Price data for fungible tokens (native, ERC20, SPL)\n * Matches V3SpotPricesResponse from the Price API.\n */\nexport type FungibleAssetPrice = BaseAssetPrice & {\n assetPriceType: 'fungible';\n /** CoinGecko ID */\n id?: string;\n /** Market capitalization */\n marketCap?: number;\n /** All-time high price */\n allTimeHigh?: number;\n /** All-time low price */\n allTimeLow?: number;\n /** 24h trading volume */\n totalVolume?: number;\n /** 24h high price */\n high1d?: number;\n /** 24h low price */\n low1d?: number;\n /** Circulating supply */\n circulatingSupply?: number;\n /** Fully diluted market cap */\n dilutedMarketCap?: number;\n /** 24h market cap change percentage */\n marketCapPercentChange1d?: number;\n /** 24h price change in USD */\n priceChange1d?: number;\n /** 1h price change percentage */\n pricePercentChange1h?: number;\n /** 24h price change percentage */\n pricePercentChange1d?: number;\n /** 7d price change percentage */\n pricePercentChange7d?: number;\n /** 14d price change percentage */\n pricePercentChange14d?: number;\n /** 30d price change percentage */\n pricePercentChange30d?: number;\n /** 200d price change percentage */\n pricePercentChange200d?: number;\n /** 1y price change percentage */\n pricePercentChange1y?: number;\n /** Current price in USD */\n usdPrice: number;\n};\n\n/**\n * Price data for NFT collections\n */\nexport type NFTAssetPrice = BaseAssetPrice & {\n assetPriceType: 'nft';\n /** Floor price */\n floorPrice?: number;\n /** Last sale price */\n lastSalePrice?: number;\n /** Collection trading volume */\n collectionVolume?: number;\n /** Average price */\n averagePrice?: number;\n /** Number of sales in 24h */\n sales24h?: number;\n};\n\n/**\n * Union type representing all possible asset price types.\n * All types must be JSON-serializable.\n */\nexport type AssetPrice = FungibleAssetPrice | NFTAssetPrice;\n\n// ============================================================================\n// BALANCE TYPES (vary by asset type)\n// ============================================================================\n\n/**\n * Balance data for fungible tokens (native, ERC20, SPL).\n */\nexport type FungibleAssetBalance = {\n /** Raw balance amount as string (e.g., \"1000000000\" for 1000 USDC) */\n amount: string;\n};\n\n/**\n * Balance data for ERC721 NFTs.\n * Each tokenId has its own CAIP-19 asset ID, so always \"1\".\n */\nexport type ERC721AssetBalance = {\n /** Always \"1\" for ERC721 (non-fungible) */\n amount: '1';\n};\n\n/**\n * Balance data for ERC1155 multi-tokens.\n */\nexport type ERC1155AssetBalance = {\n /** Quantity owned of this specific tokenId */\n amount: string;\n};\n\n/**\n * Union type representing all possible asset balance types.\n * All types must be JSON-serializable.\n */\nexport type AssetBalance =\n | FungibleAssetBalance\n | ERC721AssetBalance\n | ERC1155AssetBalance\n | { amount: string; [key: string]: Json };\n\n// ============================================================================\n// DATA SOURCE TYPES\n// ============================================================================\n\n/**\n * Data type dimension - what kind of data\n */\nexport type DataType = 'balance' | 'metadata' | 'price';\n\n/**\n * Account with its supported chains (enabled chains ∩ account scope).\n * Pre-computed by the controller so data sources do not need to implement\n * account-scope logic; they iterate over supportedChains for each account.\n */\nexport type AccountWithSupportedChains = {\n account: InternalAccount;\n supportedChains: ChainId[];\n};\n\n/**\n * Request for data from data sources\n */\nexport type DataRequest = {\n /** Accounts with their supported chains (enabled ∩ account scope). Data sources use this instead of computing accountSupportsChain. */\n accountsWithSupportedChains: AccountWithSupportedChains[];\n /** CAIP-2 chain IDs (union of chains in this request) */\n chainIds: ChainId[];\n /** Filter by asset types */\n assetTypes?: AssetType[];\n /** Which data to fetch */\n dataTypes: DataType[];\n /** Specific CAIP-19 asset IDs */\n customAssets?: Caip19AssetId[];\n /**\n * When true, the data source should poll only the user's `customAssets`\n * for the requested chains and skip refreshing the regular tracked\n * balances. Used by the AssetsController to issue a supplemental RPC\n * subscription on chains that another data source is already covering.\n */\n customAssetsOnly?: boolean;\n /** Force fresh fetch, bypass cache */\n forceUpdate?: boolean;\n /** Hint for polling interval (ms) - used by data sources that implement polling */\n updateInterval?: number;\n /** Specific CAIP-19 asset IDs for price update */\n assetsForPriceUpdate?: Caip19AssetId[];\n};\n\n/**\n * Response from data sources\n */\nexport type DataResponse = {\n /** Metadata for assets (shared across accounts) */\n assetsInfo?: Record<Caip19AssetId, AssetMetadata>;\n /** Price data for assets (shared across accounts) */\n assetsPrice?: Record<Caip19AssetId, AssetPrice>;\n /** Balance data per account */\n assetsBalance?: Record<AccountId, Record<Caip19AssetId, AssetBalance>>;\n /** Errors encountered, keyed by chain ID */\n errors?: Record<ChainId, string>;\n /** Detected assets (assets that do not have metadata) */\n detectedAssets?: Record<AccountId, Caip19AssetId[]>;\n /**\n * How to apply this response to state. See {@link AssetsUpdateMode}.\n * Defaults to `'merge'` if omitted.\n */\n updateMode?: AssetsUpdateMode;\n};\n\n/**\n * Type of {@link DataResponse.updateMode}: how the controller applies the response to state.\n *\n * - **full**: Response is the full set for the scope. Assets in state but not in the\n * response are cleared (except custom assets). Use for initial fetch or full refresh.\n * - **merge**: Only assets present in the response are updated; nothing is removed.\n * Use for event-driven or incremental updates.\n */\nexport type AssetsUpdateMode = 'full' | 'merge';\n\n// ============================================================================\n// DATA SOURCE <-> CONTROLLER (DIRECT CALLS, NO MESSENGER PER SOURCE)\n// ============================================================================\n\n/**\n * Callbacks for data sources to report to AssetsController.\n * Passed to data sources so they report by direct call instead of messenger.\n */\nexport type AssetsControllerReport = {\n onActiveChainsUpdate: (dataSourceId: string, activeChains: ChainId[]) => void;\n onAssetsUpdate: (response: DataResponse, sourceId: string) => Promise<void>;\n};\n\n/** Request passed from controller to data source when subscribing */\nexport type DataSourceSubscriptionRequest = {\n request: DataRequest;\n subscriptionId: string;\n isUpdate: boolean;\n};\n\n/**\n * Interface for balance data sources that the controller calls directly.\n * No messenger is required for controller <-> data source communication.\n */\nexport type BalanceDataSource = {\n getAssetsMiddleware: () => Middleware;\n subscribe: (request: DataSourceSubscriptionRequest) => Promise<void>;\n unsubscribe: (subscriptionId: string) => Promise<void>;\n getName: () => string;\n};\n\n/**\n * Interface for the price data source (subscribe/unsubscribe + middleware).\n * Controller calls these methods directly.\n */\nexport type PriceDataSourceInterface = {\n getAssetsMiddleware: () => Middleware;\n subscribe: (request: DataSourceSubscriptionRequest) => Promise<void>;\n unsubscribe: (subscriptionId: string) => Promise<void>;\n};\n\n/**\n * Middleware-only source (e.g. detection, token enrichment).\n * Controller calls getAssetsMiddleware() directly.\n */\nexport type MiddlewareDataSource = {\n getAssetsMiddleware: () => Middleware;\n};\n\n// ============================================================================\n// UNIFIED MIDDLEWARE TYPES\n// ============================================================================\n\n/**\n * Internal state structure for AssetsController following normalized design.\n *\n * Keys use CAIP identifiers:\n * - assetsInfo keys: CAIP-19 asset IDs (e.g., \"eip155:1/erc20:0x...\")\n * - assetsBalance outer keys: Account IDs (InternalAccount.id UUIDs)\n * - assetsBalance inner keys: CAIP-19 asset IDs\n * - assetsPrice keys: CAIP-19 asset IDs\n * - customAssets outer keys: Account IDs (InternalAccount.id UUIDs)\n * - customAssets inner values: CAIP-19 asset IDs array\n * - assetPreferences keys: CAIP-19 asset IDs\n */\nexport type AssetsControllerStateInternal = {\n /** Shared metadata for all assets (stored once per asset) */\n assetsInfo: Record<Caip19AssetId, AssetMetadata>;\n /** Per-account balance data */\n assetsBalance: Record<AccountId, Record<Caip19AssetId, AssetBalance>>;\n /** Price data for assets */\n assetsPrice: Record<Caip19AssetId, AssetPrice>;\n /** Custom assets added by users per account */\n customAssets: Record<AccountId, Caip19AssetId[]>;\n /** UI preferences per asset (e.g. hidden) - separate from metadata */\n assetPreferences: Record<Caip19AssetId, AssetPreferences>;\n};\n\n/**\n * Base context for all middleware operations.\n * Contains the common interface shared by fetch and subscribe.\n */\nexport type Context = {\n /** The data request */\n request: DataRequest;\n /** The response data (mutated by middlewares) */\n response: DataResponse;\n /** Get current assets state */\n getAssetsState: () => AssetsControllerStateInternal;\n /**\n * Optional breakdown of latency (ms) per data source, e.g. from parallel\n * middlewares. Keys are source names (often \"MiddlewareName.SourceName\").\n * Merged into the controller's durationByDataSource for tracing.\n */\n durationByDataSource?: Record<string, number>;\n};\n\n/**\n * Next function for middleware chain\n */\nexport type NextFunction = (context: Context) => Promise<Context>;\n\n/**\n * Middleware function - works for both fetch and subscribe operations.\n */\nexport type Middleware = (\n context: Context,\n next: NextFunction,\n) => Promise<Context>;\n\n/**\n * An assets data source: any object that can participate in the assets\n * middleware chain with a display name (e.g. for tracing). Used by\n * createParallelMiddleware and by the controller when executing the chain.\n */\nexport type AssetsDataSource = {\n getName(): string;\n assetsMiddleware: Middleware;\n};\n\n/**\n * Wraps a middleware to only execute if specific dataTypes are requested.\n *\n * @param dataTypes - DataTypes that must be in the request for middleware to run\n * @param middleware - The middleware to conditionally execute\n * @returns A middleware that skips execution if none of the dataTypes are requested\n *\n * @example\n * ```typescript\n * // Only runs for metadata requests\n * const metadataMiddleware = forDataTypes(['metadata'], async (ctx, next) => {\n * const result = await next(ctx);\n * // Enrich metadata...\n * return result;\n * });\n *\n * // Runs for balance or price requests\n * const balanceOrPriceMiddleware = forDataTypes(['balance', 'price'], async (ctx, next) => {\n * const result = await next(ctx);\n * // Process balances or prices...\n * return result;\n * });\n * ```\n */\nexport function forDataTypes(\n dataTypes: DataType[],\n middleware: Middleware,\n): Middleware {\n return async (ctx, next) => {\n const requestedTypes = ctx.request.dataTypes;\n const shouldRun = dataTypes.some((dt) => requestedTypes.includes(dt));\n\n if (!shouldRun) {\n return next(ctx);\n }\n\n return middleware(ctx, next);\n };\n}\n\n/**\n * Context for fetch operations.\n * Extends base Context - no additional fields needed for fetch.\n */\nexport type FetchContext = Context;\n\n// Legacy aliases for backwards compatibility\nexport type FetchNextFunction = NextFunction;\nexport type FetchMiddleware = Middleware;\n\n/**\n * Subscription response returned when subscribing to asset updates.\n */\nexport type SubscriptionResponse = {\n /** Chains actively subscribed */\n chains: ChainId[];\n /** Account ID being watched */\n accountId: AccountId;\n /** Asset types being watched */\n assetTypes: AssetType[];\n /** Data types being kept fresh */\n dataTypes: DataType[];\n /** Cleanup function */\n unsubscribe: () => void;\n};\n\n// ============================================================================\n// COMBINED ASSET TYPE (for UI)\n// ============================================================================\n\n/**\n * Combined asset type matching state structure: balance, metadata, price\n */\nexport type Asset = {\n /** CAIP-19 asset ID */\n id: Caip19AssetId;\n /** CAIP-2 chain ID (extracted from id) */\n chainId: ChainId;\n /** Balance data */\n balance: AssetBalance;\n /** Metadata (symbol, name, decimals, etc.) */\n metadata: AssetMetadata;\n /** Price data */\n price: AssetPrice;\n /** Computed fiat value (balance × price) */\n fiatValue: number;\n};\n\n// ============================================================================\n// EVENT TYPES\n// ============================================================================\n\n/**\n * Event emitted when balances change\n */\nexport type BalanceChangeEvent = {\n accountId: AccountId;\n assetId: Caip19AssetId;\n previousAmount: string;\n newAmount: string;\n timestamp: number;\n};\n\n/**\n * Event emitted when prices change\n */\nexport type PriceChangeEvent = {\n assetIds: Caip19AssetId[];\n timestamp: number;\n};\n\n/**\n * Event emitted when metadata changes\n */\nexport type MetadataChangeEvent = {\n assetId: Caip19AssetId;\n changes: Partial<AssetMetadata>;\n};\n\n/**\n * Event emitted when assets without metadata are detected\n */\nexport type AssetsDetectedEvent = {\n accountId: AccountId;\n assetIds: Caip19AssetId[];\n};\n"]}
1
+ {"version":3,"file":"types.mjs","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAygBA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CAC1B,SAAqB,EACrB,UAAsB;IAEtB,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,MAAM,cAAc,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC;QAC7C,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;QAEtE,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,OAAO,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import type { InternalAccount } from '@metamask/keyring-internal-api';\nimport type { CaipAssetType, CaipChainId, Json } from '@metamask/utils';\n\n/**\n * CAIP-19 compliant asset identifier\n * Format: \"{chainId}/{assetNamespace}:{assetReference}[/tokenId]\"\n *\n * Examples:\n * - Native: \"eip155:1/slip44:60\" (ETH)\n * - ERC20: \"eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48\" (USDC)\n * - ERC721: \"eip155:1/erc721:0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D/1234\" (BAYC #1234)\n * - SPL: \"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\"\n */\nexport type Caip19AssetId = CaipAssetType;\n\n/**\n * InternalAccount UUID from AccountsController\n * Not the blockchain address!\n */\nexport type AccountId = string;\n\n/**\n * CAIP-2 chain identifier\n */\nexport type ChainId = CaipChainId;\n\n// ============================================================================\n// ASSET TYPES - Defined by metadata structure\n// ============================================================================\n\n/**\n * Asset types define the metadata structure, not blockchain implementation.\n * - \"fungible\" includes: native, erc20, spl - all share balance, symbol, decimals\n * - \"nft\" includes: erc721, erc1155 - include tokenId, image, attributes\n */\nexport type AssetType = 'fungible' | 'nft' | 'collectible';\n\n/**\n * Token standards - blockchain implementation details\n */\nexport type TokenStandard =\n | 'native'\n | 'erc20'\n | 'erc721'\n | 'erc1155'\n | 'spl'\n | string;\n\n// ============================================================================\n// METADATA TYPES (vary by asset type)\n// ============================================================================\n\n/**\n * UI preferences for an asset (stored in assetPreferences state, not in metadata).\n */\nexport type AssetPreferences = {\n /** Whether the asset is hidden from display */\n hidden?: boolean;\n};\n\n/**\n * Base metadata attributes shared by ALL asset types.\n */\nexport type BaseAssetMetadata = {\n /** Token standard - how it's implemented on the blockchain */\n type: TokenStandard;\n /** Display symbol (e.g., \"ETH\", \"USDC\") */\n symbol: string;\n /** Full name (e.g., \"Ethereum\", \"USD Coin\") */\n name: string;\n /** Token decimals (18 for ETH, 6 for USDC, etc.) */\n decimals: number;\n /** Logo URL or data URI */\n image?: string;\n};\n\n// ============================================================================\n// TOKEN CONTRACT DATA TYPES\n// ============================================================================\n\n/** Fee information for token transfers */\nexport type TokenFees = {\n avgFee: number;\n maxFee: number;\n minFee: number;\n};\n\n/** Honeypot detection status */\nexport type HoneypotStatus = {\n honeypotIs: boolean;\n goPlus?: boolean;\n};\n\n/** Storage slot information for the contract */\nexport type StorageSlots = {\n balance: number;\n approval: number;\n};\n\n/** Localized description */\nexport type LocalizedDescription = {\n en: string;\n};\n\n// ============================================================================\n// ASSET METADATA TYPES\n// ============================================================================\n\n/**\n * Metadata for fungible tokens.\n * Structure mirrors V3AssetResponse from the Tokens API.\n *\n * Differences from V3AssetResponse:\n * - `type` is derived from assetId namespace (not in API response)\n * - `image` maps from API's `iconUrl`\n * - `assetId` is not stored (used as the key)\n */\nexport type FungibleAssetMetadata = {\n /** Token type derived from assetId namespace */\n type: 'native' | 'erc20' | 'spl';\n /** CoinGecko ID for price lookups */\n coingeckoId?: string;\n /** Number of token list occurrences */\n occurrences?: number;\n /** DEX/aggregator integrations */\n aggregators?: string[];\n /** Asset labels/tags (e.g., \"stable_coin\") */\n labels?: string[];\n /** Whether the token supports ERC-20 permit */\n erc20Permit?: boolean;\n /** Fee information for token transfers */\n fees?: TokenFees;\n /** Honeypot detection status */\n honeypotStatus?: HoneypotStatus;\n /** Storage slot information for the contract */\n storage?: StorageSlots;\n /** Whether the contract is verified */\n isContractVerified?: boolean;\n /** Localized description */\n description?: LocalizedDescription;\n} & BaseAssetMetadata;\n\n/**\n * Metadata for ERC721 NFTs\n * Asset Type: \"nft\"\n */\nexport type ERC721AssetMetadata = {\n type: 'erc721';\n decimals: 0;\n /** Collection name */\n collectionName?: string;\n /** Collection size */\n collectionSize?: number;\n /** NFT traits/attributes - must be Json-serializable */\n traits?: Record<string, Json>;\n /** Rarity score */\n rarity?: number;\n /** Verification status */\n verified?: boolean;\n} & BaseAssetMetadata;\n\n/**\n * Metadata for ERC1155 multi-tokens\n */\nexport type ERC1155AssetMetadata = {\n type: 'erc1155';\n /** Token URI */\n tokenUri?: string;\n /** Token category */\n category?: string;\n /** Spam detection flag */\n isSpam?: boolean;\n} & BaseAssetMetadata;\n\n/**\n * Union type representing all possible asset metadata types.\n * All types must be JSON-serializable.\n */\nexport type AssetMetadata =\n | FungibleAssetMetadata\n | ERC721AssetMetadata\n | ERC1155AssetMetadata\n | (BaseAssetMetadata & { [key: string]: Json });\n\n// ============================================================================\n// PRICE TYPES (vary by asset type)\n// ============================================================================\n\n/**\n * Base price attributes.\n */\nexport type BaseAssetPrice = {\n /** Current price in selected currency */\n price: number;\n /** Timestamp of last price update */\n lastUpdated: number;\n};\n\n/**\n * Price data for fungible tokens (native, ERC20, SPL)\n * Matches V3SpotPricesResponse from the Price API.\n */\nexport type FungibleAssetPrice = BaseAssetPrice & {\n assetPriceType: 'fungible';\n /** CoinGecko ID */\n id?: string;\n /** Market capitalization */\n marketCap?: number;\n /** All-time high price */\n allTimeHigh?: number;\n /** All-time low price */\n allTimeLow?: number;\n /** 24h trading volume */\n totalVolume?: number;\n /** 24h high price */\n high1d?: number;\n /** 24h low price */\n low1d?: number;\n /** Circulating supply */\n circulatingSupply?: number;\n /** Fully diluted market cap */\n dilutedMarketCap?: number;\n /** 24h market cap change percentage */\n marketCapPercentChange1d?: number;\n /** 24h price change in USD */\n priceChange1d?: number;\n /** 1h price change percentage */\n pricePercentChange1h?: number;\n /** 24h price change percentage */\n pricePercentChange1d?: number;\n /** 7d price change percentage */\n pricePercentChange7d?: number;\n /** 14d price change percentage */\n pricePercentChange14d?: number;\n /** 30d price change percentage */\n pricePercentChange30d?: number;\n /** 200d price change percentage */\n pricePercentChange200d?: number;\n /** 1y price change percentage */\n pricePercentChange1y?: number;\n /** Current price in USD */\n usdPrice: number;\n};\n\n/**\n * Price data for NFT collections\n */\nexport type NFTAssetPrice = BaseAssetPrice & {\n assetPriceType: 'nft';\n /** Floor price */\n floorPrice?: number;\n /** Last sale price */\n lastSalePrice?: number;\n /** Collection trading volume */\n collectionVolume?: number;\n /** Average price */\n averagePrice?: number;\n /** Number of sales in 24h */\n sales24h?: number;\n};\n\n/**\n * Union type representing all possible asset price types.\n * All types must be JSON-serializable.\n */\nexport type AssetPrice = FungibleAssetPrice | NFTAssetPrice;\n\n// ============================================================================\n// BALANCE TYPES (vary by asset type)\n// ============================================================================\n\n/**\n * Balance data for fungible tokens (native, ERC20, SPL).\n */\nexport type FungibleAssetBalance = {\n /** Raw balance amount as string (e.g., \"1000000000\" for 1000 USDC) */\n amount: string;\n};\n\n/**\n * Balance data for ERC721 NFTs.\n * Each tokenId has its own CAIP-19 asset ID, so always \"1\".\n */\nexport type ERC721AssetBalance = {\n /** Always \"1\" for ERC721 (non-fungible) */\n amount: '1';\n};\n\n/**\n * Balance data for ERC1155 multi-tokens.\n */\nexport type ERC1155AssetBalance = {\n /** Quantity owned of this specific tokenId */\n amount: string;\n};\n\n/**\n * Union type representing all possible asset balance types.\n * All types must be JSON-serializable.\n */\nexport type AssetBalance =\n | FungibleAssetBalance\n | ERC721AssetBalance\n | ERC1155AssetBalance\n | { amount: string; [key: string]: Json };\n\n// ============================================================================\n// DATA SOURCE TYPES\n// ============================================================================\n\n/**\n * Data type dimension - what kind of data\n */\nexport type DataType = 'balance' | 'metadata' | 'price';\n\n/**\n * Account with its supported chains (enabled chains ∩ account scope).\n * Pre-computed by the controller so data sources do not need to implement\n * account-scope logic; they iterate over supportedChains for each account.\n */\nexport type AccountWithSupportedChains = {\n account: InternalAccount;\n supportedChains: ChainId[];\n};\n\n/**\n * Request for data from data sources\n */\nexport type DataRequest = {\n /** Accounts with their supported chains (enabled ∩ account scope). Data sources use this instead of computing accountSupportsChain. */\n accountsWithSupportedChains: AccountWithSupportedChains[];\n /** CAIP-2 chain IDs (union of chains in this request) */\n chainIds: ChainId[];\n /** Filter by asset types */\n assetTypes?: AssetType[];\n /** Which data to fetch */\n dataTypes: DataType[];\n /** Specific CAIP-19 asset IDs */\n customAssets?: Caip19AssetId[];\n /**\n * When true, the data source should poll only the user's `customAssets`\n * for the requested chains and skip refreshing the regular tracked\n * balances. Used by the AssetsController to issue a supplemental RPC\n * subscription on chains that another data source is already covering.\n */\n customAssetsOnly?: boolean;\n /** Force fresh fetch, bypass cache */\n forceUpdate?: boolean;\n /** Hint for polling interval (ms) - used by data sources that implement polling */\n updateInterval?: number;\n /** Specific CAIP-19 asset IDs for price update */\n assetsForPriceUpdate?: Caip19AssetId[];\n};\n\n/**\n * Response from data sources\n */\nexport type DataResponse = {\n /** Metadata for assets (shared across accounts) */\n assetsInfo?: Record<Caip19AssetId, AssetMetadata>;\n /** Price data for assets (shared across accounts) */\n assetsPrice?: Record<Caip19AssetId, AssetPrice>;\n /** Balance data per account */\n assetsBalance?: Record<AccountId, Record<Caip19AssetId, AssetBalance>>;\n /** Errors encountered, keyed by chain ID */\n errors?: Record<ChainId, string>;\n /** Detected assets (assets that do not have metadata) */\n detectedAssets?: Record<AccountId, Caip19AssetId[]>;\n /**\n * How to apply this response to state. See {@link AssetsUpdateMode}.\n * Defaults to `'merge'` if omitted.\n */\n updateMode?: AssetsUpdateMode;\n /**\n * When set with `updateMode: 'merge'`, balances on chains present in\n * `assetsBalance` replace the prior chain slice (stale tokens on those chains\n * are dropped). Custom assets on covered chains are preserved. Used for\n * `getAssets({ forceUpdate: true })` so unlock/startup reflects the API snapshot\n * without switching to `updateMode: 'full'`.\n */\n replaceCoveredChainBalances?: boolean;\n};\n\n/**\n * Type of {@link DataResponse.updateMode}: how the controller applies the response to state.\n *\n * - **full**: Response is the full set for the scope. Assets in state but not in the\n * response are cleared (except custom assets). Use for initial fetch or full refresh.\n * - **merge**: By default only assets present in the response are updated;\n * nothing is removed. When {@link DataResponse.replaceCoveredChainBalances}\n * is true, balances on chains present in the response replace the prior chain\n * slice (stale tokens on those chains are dropped; custom assets preserved).\n * Metadata and prices from the response are applied. Use for event-driven updates.\n * - **update**: Balance-only overlay — incoming balance amounts are patched in place;\n * existing balances, metadata, and prices are never removed or overwritten.\n * Missing metadata and prices from the response are seeded so RPC-only chains\n * can render on first fetch. Use for force refresh when the API may return a\n * partial chain snapshot.\n */\nexport type AssetsUpdateMode = 'full' | 'merge' | 'update';\n\n// ============================================================================\n// DATA SOURCE <-> CONTROLLER (DIRECT CALLS, NO MESSENGER PER SOURCE)\n// ============================================================================\n\n/**\n * Callbacks for data sources to report to AssetsController.\n * Passed to data sources so they report by direct call instead of messenger.\n */\nexport type AssetsControllerReport = {\n onActiveChainsUpdate: (dataSourceId: string, activeChains: ChainId[]) => void;\n onAssetsUpdate: (response: DataResponse, sourceId: string) => Promise<void>;\n};\n\n/** Request passed from controller to data source when subscribing */\nexport type DataSourceSubscriptionRequest = {\n request: DataRequest;\n subscriptionId: string;\n isUpdate: boolean;\n};\n\n/**\n * Interface for balance data sources that the controller calls directly.\n * No messenger is required for controller <-> data source communication.\n */\nexport type BalanceDataSource = {\n getAssetsMiddleware: () => Middleware;\n subscribe: (request: DataSourceSubscriptionRequest) => Promise<void>;\n unsubscribe: (subscriptionId: string) => Promise<void>;\n getName: () => string;\n};\n\n/**\n * Interface for the price data source (subscribe/unsubscribe + middleware).\n * Controller calls these methods directly.\n */\nexport type PriceDataSourceInterface = {\n getAssetsMiddleware: () => Middleware;\n subscribe: (request: DataSourceSubscriptionRequest) => Promise<void>;\n unsubscribe: (subscriptionId: string) => Promise<void>;\n};\n\n/**\n * Middleware-only source (e.g. detection, token enrichment).\n * Controller calls getAssetsMiddleware() directly.\n */\nexport type MiddlewareDataSource = {\n getAssetsMiddleware: () => Middleware;\n};\n\n// ============================================================================\n// UNIFIED MIDDLEWARE TYPES\n// ============================================================================\n\n/**\n * Internal state structure for AssetsController following normalized design.\n *\n * Keys use CAIP identifiers:\n * - assetsInfo keys: CAIP-19 asset IDs (e.g., \"eip155:1/erc20:0x...\")\n * - assetsBalance outer keys: Account IDs (InternalAccount.id UUIDs)\n * - assetsBalance inner keys: CAIP-19 asset IDs\n * - assetsPrice keys: CAIP-19 asset IDs\n * - customAssets outer keys: Account IDs (InternalAccount.id UUIDs)\n * - customAssets inner values: CAIP-19 asset IDs array\n * - assetPreferences keys: CAIP-19 asset IDs\n */\nexport type AssetsControllerStateInternal = {\n /** Shared metadata for all assets (stored once per asset) */\n assetsInfo: Record<Caip19AssetId, AssetMetadata>;\n /** Per-account balance data */\n assetsBalance: Record<AccountId, Record<Caip19AssetId, AssetBalance>>;\n /** Price data for assets */\n assetsPrice: Record<Caip19AssetId, AssetPrice>;\n /** Custom assets added by users per account */\n customAssets: Record<AccountId, Caip19AssetId[]>;\n /** UI preferences per asset (e.g. hidden) - separate from metadata */\n assetPreferences: Record<Caip19AssetId, AssetPreferences>;\n};\n\n/**\n * Base context for all middleware operations.\n * Contains the common interface shared by fetch and subscribe.\n */\nexport type Context = {\n /** The data request */\n request: DataRequest;\n /** The response data (mutated by middlewares) */\n response: DataResponse;\n /** Get current assets state */\n getAssetsState: () => AssetsControllerStateInternal;\n /**\n * Optional breakdown of latency (ms) per data source, e.g. from parallel\n * middlewares. Keys are source names (often \"MiddlewareName.SourceName\").\n * Merged into the controller's durationByDataSource for tracing.\n */\n durationByDataSource?: Record<string, number>;\n};\n\n/**\n * Next function for middleware chain\n */\nexport type NextFunction = (context: Context) => Promise<Context>;\n\n/**\n * Middleware function - works for both fetch and subscribe operations.\n */\nexport type Middleware = (\n context: Context,\n next: NextFunction,\n) => Promise<Context>;\n\n/**\n * An assets data source: any object that can participate in the assets\n * middleware chain with a display name (e.g. for tracing). Used by\n * createParallelMiddleware and by the controller when executing the chain.\n */\nexport type AssetsDataSource = {\n getName(): string;\n assetsMiddleware: Middleware;\n};\n\n/**\n * Wraps a middleware to only execute if specific dataTypes are requested.\n *\n * @param dataTypes - DataTypes that must be in the request for middleware to run\n * @param middleware - The middleware to conditionally execute\n * @returns A middleware that skips execution if none of the dataTypes are requested\n *\n * @example\n * ```typescript\n * // Only runs for metadata requests\n * const metadataMiddleware = forDataTypes(['metadata'], async (ctx, next) => {\n * const result = await next(ctx);\n * // Enrich metadata...\n * return result;\n * });\n *\n * // Runs for balance or price requests\n * const balanceOrPriceMiddleware = forDataTypes(['balance', 'price'], async (ctx, next) => {\n * const result = await next(ctx);\n * // Process balances or prices...\n * return result;\n * });\n * ```\n */\nexport function forDataTypes(\n dataTypes: DataType[],\n middleware: Middleware,\n): Middleware {\n return async (ctx, next) => {\n const requestedTypes = ctx.request.dataTypes;\n const shouldRun = dataTypes.some((dt) => requestedTypes.includes(dt));\n\n if (!shouldRun) {\n return next(ctx);\n }\n\n return middleware(ctx, next);\n };\n}\n\n/**\n * Context for fetch operations.\n * Extends base Context - no additional fields needed for fetch.\n */\nexport type FetchContext = Context;\n\n// Legacy aliases for backwards compatibility\nexport type FetchNextFunction = NextFunction;\nexport type FetchMiddleware = Middleware;\n\n/**\n * Subscription response returned when subscribing to asset updates.\n */\nexport type SubscriptionResponse = {\n /** Chains actively subscribed */\n chains: ChainId[];\n /** Account ID being watched */\n accountId: AccountId;\n /** Asset types being watched */\n assetTypes: AssetType[];\n /** Data types being kept fresh */\n dataTypes: DataType[];\n /** Cleanup function */\n unsubscribe: () => void;\n};\n\n// ============================================================================\n// COMBINED ASSET TYPE (for UI)\n// ============================================================================\n\n/**\n * Combined asset type matching state structure: balance, metadata, price\n */\nexport type Asset = {\n /** CAIP-19 asset ID */\n id: Caip19AssetId;\n /** CAIP-2 chain ID (extracted from id) */\n chainId: ChainId;\n /** Balance data */\n balance: AssetBalance;\n /** Metadata (symbol, name, decimals, etc.) */\n metadata: AssetMetadata;\n /** Price data */\n price: AssetPrice;\n /** Computed fiat value (balance × price) */\n fiatValue: number;\n};\n\n// ============================================================================\n// EVENT TYPES\n// ============================================================================\n\n/**\n * Event emitted when balances change\n */\nexport type BalanceChangeEvent = {\n accountId: AccountId;\n assetId: Caip19AssetId;\n previousAmount: string;\n newAmount: string;\n timestamp: number;\n};\n\n/**\n * Event emitted when prices change\n */\nexport type PriceChangeEvent = {\n assetIds: Caip19AssetId[];\n timestamp: number;\n};\n\n/**\n * Event emitted when metadata changes\n */\nexport type MetadataChangeEvent = {\n assetId: Caip19AssetId;\n changes: Partial<AssetMetadata>;\n};\n\n/**\n * Event emitted when assets without metadata are detected\n */\nexport type AssetsDetectedEvent = {\n accountId: AccountId;\n assetIds: Caip19AssetId[];\n};\n"]}
@@ -85,6 +85,17 @@ class DedupingBatchFetcher {
85
85
  invalidate() {
86
86
  __classPrivateFieldGet(this, _DedupingBatchFetcher_fetchedAt, "f").clear();
87
87
  }
88
+ /**
89
+ * Clear freshness for specific keys only, forcing the next fetch to
90
+ * re-request those keys regardless of TTL. Does not affect inflight fetches.
91
+ *
92
+ * @param keys - Keys to mark stale.
93
+ */
94
+ invalidateKeys(keys) {
95
+ for (const key of keys) {
96
+ __classPrivateFieldGet(this, _DedupingBatchFetcher_fetchedAt, "f").delete(key);
97
+ }
98
+ }
88
99
  /** Clear all freshness and inflight state. */
89
100
  destroy() {
90
101
  __classPrivateFieldGet(this, _DedupingBatchFetcher_fetchedAt, "f").clear();
@@ -1 +1 @@
1
- {"version":3,"file":"dedupingBatchFetcher.cjs","sourceRoot":"","sources":["../../src/utils/dedupingBatchFetcher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Executes the underlying batched fetch for a set of keys. Only keys that have\n * a value need to be present in the returned record; keys the source had no\n * data for are simply omitted (they are still marked fresh — see\n * {@link DedupingBatchFetcher.fetch}).\n *\n * @param keys - The keys to fetch (already filtered to stale, not-inflight keys).\n * @returns The fetched values keyed by key.\n */\nexport type BatchFetchFn<Key extends string, Value> = (\n keys: Key[],\n) => Promise<Record<Key, Value>>;\n\nexport type DedupingBatchFetcherOptions<Key extends string, Value> = {\n /** Performs the actual batched fetch for stale, not-inflight keys. */\n fetchBatch: BatchFetchFn<Key, Value>;\n /**\n * Minimum age (ms) before a key is considered stale and re-fetched. Keys\n * fetched more recently than this are skipped entirely.\n */\n freshnessTtlMs: number;\n};\n\n/**\n * Deduplicates batched fetches by key across two dimensions:\n *\n * 1. **Freshness TTL** — keys fetched within `freshnessTtlMs` are skipped\n * entirely. Note the freshness window only starts once a fetch *completes*,\n * so it does not cover requests that are still in flight.\n * 2. **Inflight coalescing** — if a fetch is already in progress for a key,\n * concurrent callers join the existing promise instead of issuing a new\n * request. This covers the request-in-flight window the freshness TTL\n * structurally cannot.\n *\n * Both layers are per-key, so a call for a partially-overlapping set of keys\n * reuses the fresh/inflight keys and only fetches the genuinely-missing ones.\n *\n * The fetch is batched: all stale, not-inflight keys from a single `fetch()`\n * call are passed to `fetchBatch` together, then split into per-key results so\n * each key can be joined independently.\n */\nexport class DedupingBatchFetcher<Key extends string, Value> {\n readonly #fetchBatch: BatchFetchFn<Key, Value>;\n\n #freshnessTtlMs: number;\n\n /** Tracks the last successful fetch time per key (freshness gating). */\n readonly #fetchedAt = new Map<Key, number>();\n\n /**\n * Per-key inflight fetch promises. Each resolves to the value, or `undefined`\n * if the batch failed or returned no data for that key.\n */\n readonly #inflight = new Map<Key, Promise<Value | undefined>>();\n\n constructor(options: DedupingBatchFetcherOptions<Key, Value>) {\n this.#fetchBatch = options.fetchBatch;\n this.#freshnessTtlMs = options.freshnessTtlMs;\n }\n\n /**\n * Minimum age (ms) before a key is re-fetched.\n *\n * @returns The current freshness TTL in milliseconds.\n */\n get freshnessTtlMs(): number {\n return this.#freshnessTtlMs;\n }\n\n set freshnessTtlMs(ms: number) {\n this.#freshnessTtlMs = ms;\n }\n\n /**\n * Fetch values for the given keys, deduplicating against fresh and inflight\n * fetches.\n *\n * @param keys - The keys to fetch.\n * @returns Values keyed by key. Only contains entries for keys that were\n * actually fetched (or joined from inflight) and had a value.\n */\n async fetch(keys: Key[]): Promise<Record<Key, Value>> {\n const { staleKeys, inflightKeys } = this.#partition(keys);\n\n if (staleKeys.length === 0 && inflightKeys.length === 0) {\n return {} as Record<Key, Value>;\n }\n\n // Start a fetch for stale keys and join any fetches already in progress.\n const batchPromise =\n staleKeys.length > 0 ? this.#startBatchFetch(staleKeys) : undefined;\n const values = await this.#joinInflight(inflightKeys);\n\n if (batchPromise) {\n Object.assign(values, await batchPromise);\n }\n\n return values;\n }\n\n /**\n * Clear the freshness cache, forcing the next fetch to re-request every key\n * regardless of TTL. Does not affect inflight fetches.\n */\n invalidate(): void {\n this.#fetchedAt.clear();\n }\n\n /** Clear all freshness and inflight state. */\n destroy(): void {\n this.#fetchedAt.clear();\n this.#inflight.clear();\n }\n\n /**\n * Split keys into those that need a fresh fetch and those already being\n * fetched by another caller. Keys still within the freshness TTL are dropped.\n *\n * @param keys - The keys to classify.\n * @returns `staleKeys` (need fetching) and `inflightKeys` (join existing fetch).\n */\n #partition(keys: Key[]): { staleKeys: Key[]; inflightKeys: Key[] } {\n const now = Date.now();\n const staleKeys: Key[] = [];\n const inflightKeys: Key[] = [];\n\n for (const key of keys) {\n if (this.#isFresh(key, now)) {\n continue;\n }\n if (this.#inflight.has(key)) {\n inflightKeys.push(key);\n } else {\n staleKeys.push(key);\n }\n }\n\n return { staleKeys, inflightKeys };\n }\n\n /**\n * Returns true if the key's last fetch is still within the freshness TTL.\n *\n * @param key - The key to check.\n * @param now - Current timestamp (avoids repeated clock reads).\n * @returns True if the key was fetched within the freshness TTL.\n */\n #isFresh(key: Key, now: number): boolean {\n const fetchedAt = this.#fetchedAt.get(key);\n return fetchedAt !== undefined && now - fetchedAt < this.#freshnessTtlMs;\n }\n\n /**\n * Launch a batch fetch and register a per-key inflight promise for each key\n * so concurrent callers can join. On success, all requested keys are marked\n * fresh — including keys the source returned no value for, since the absence\n * of a value is itself a valid answer that should not be re-asked until the\n * TTL expires. A failed batch leaves keys stale so they are retried.\n *\n * @param staleKeys - Keys to fetch (none of which are already inflight).\n * @returns The batch fetch promise.\n */\n #startBatchFetch(staleKeys: Key[]): Promise<Record<Key, Value>> {\n const batchPromise = this.#fetchBatch(staleKeys).then((values) => {\n const fetchedAt = Date.now();\n for (const key of staleKeys) {\n this.#fetchedAt.set(key, fetchedAt);\n }\n return values;\n });\n\n for (const key of staleKeys) {\n const perKey = batchPromise.then(\n (values) => values[key],\n () => undefined,\n );\n this.#inflight.set(key, perKey);\n }\n\n // Clean up inflight entries once the batch settles (success or failure).\n // Rejection is already surfaced to the caller via the returned batchPromise.\n batchPromise\n .finally(() => {\n for (const key of staleKeys) {\n this.#inflight.delete(key);\n }\n })\n .catch(() => undefined);\n\n return batchPromise;\n }\n\n /**\n * Join the inflight fetches for the given keys and collect their values. Keys\n * whose inflight fetch produced no value are omitted.\n *\n * @param inflightKeys - Keys whose fetches are already in progress.\n * @returns Values keyed by key.\n */\n async #joinInflight(inflightKeys: Key[]): Promise<Record<Key, Value>> {\n const values = {} as Record<Key, Value>;\n\n const results = await Promise.all(\n inflightKeys.map(async (key) => {\n const value = await this.#inflight.get(key);\n return [key, value] as const;\n }),\n );\n\n for (const [key, value] of results) {\n if (value !== undefined) {\n values[key] = value;\n }\n }\n\n return values;\n }\n}\n"]}
1
+ {"version":3,"file":"dedupingBatchFetcher.cjs","sourceRoot":"","sources":["../../src/utils/dedupingBatchFetcher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Executes the underlying batched fetch for a set of keys. Only keys that have\n * a value need to be present in the returned record; keys the source had no\n * data for are simply omitted (they are still marked fresh — see\n * {@link DedupingBatchFetcher.fetch}).\n *\n * @param keys - The keys to fetch (already filtered to stale, not-inflight keys).\n * @returns The fetched values keyed by key.\n */\nexport type BatchFetchFn<Key extends string, Value> = (\n keys: Key[],\n) => Promise<Record<Key, Value>>;\n\nexport type DedupingBatchFetcherOptions<Key extends string, Value> = {\n /** Performs the actual batched fetch for stale, not-inflight keys. */\n fetchBatch: BatchFetchFn<Key, Value>;\n /**\n * Minimum age (ms) before a key is considered stale and re-fetched. Keys\n * fetched more recently than this are skipped entirely.\n */\n freshnessTtlMs: number;\n};\n\n/**\n * Deduplicates batched fetches by key across two dimensions:\n *\n * 1. **Freshness TTL** — keys fetched within `freshnessTtlMs` are skipped\n * entirely. Note the freshness window only starts once a fetch *completes*,\n * so it does not cover requests that are still in flight.\n * 2. **Inflight coalescing** — if a fetch is already in progress for a key,\n * concurrent callers join the existing promise instead of issuing a new\n * request. This covers the request-in-flight window the freshness TTL\n * structurally cannot.\n *\n * Both layers are per-key, so a call for a partially-overlapping set of keys\n * reuses the fresh/inflight keys and only fetches the genuinely-missing ones.\n *\n * The fetch is batched: all stale, not-inflight keys from a single `fetch()`\n * call are passed to `fetchBatch` together, then split into per-key results so\n * each key can be joined independently.\n */\nexport class DedupingBatchFetcher<Key extends string, Value> {\n readonly #fetchBatch: BatchFetchFn<Key, Value>;\n\n #freshnessTtlMs: number;\n\n /** Tracks the last successful fetch time per key (freshness gating). */\n readonly #fetchedAt = new Map<Key, number>();\n\n /**\n * Per-key inflight fetch promises. Each resolves to the value, or `undefined`\n * if the batch failed or returned no data for that key.\n */\n readonly #inflight = new Map<Key, Promise<Value | undefined>>();\n\n constructor(options: DedupingBatchFetcherOptions<Key, Value>) {\n this.#fetchBatch = options.fetchBatch;\n this.#freshnessTtlMs = options.freshnessTtlMs;\n }\n\n /**\n * Minimum age (ms) before a key is re-fetched.\n *\n * @returns The current freshness TTL in milliseconds.\n */\n get freshnessTtlMs(): number {\n return this.#freshnessTtlMs;\n }\n\n set freshnessTtlMs(ms: number) {\n this.#freshnessTtlMs = ms;\n }\n\n /**\n * Fetch values for the given keys, deduplicating against fresh and inflight\n * fetches.\n *\n * @param keys - The keys to fetch.\n * @returns Values keyed by key. Only contains entries for keys that were\n * actually fetched (or joined from inflight) and had a value.\n */\n async fetch(keys: Key[]): Promise<Record<Key, Value>> {\n const { staleKeys, inflightKeys } = this.#partition(keys);\n\n if (staleKeys.length === 0 && inflightKeys.length === 0) {\n return {} as Record<Key, Value>;\n }\n\n // Start a fetch for stale keys and join any fetches already in progress.\n const batchPromise =\n staleKeys.length > 0 ? this.#startBatchFetch(staleKeys) : undefined;\n const values = await this.#joinInflight(inflightKeys);\n\n if (batchPromise) {\n Object.assign(values, await batchPromise);\n }\n\n return values;\n }\n\n /**\n * Clear the freshness cache, forcing the next fetch to re-request every key\n * regardless of TTL. Does not affect inflight fetches.\n */\n invalidate(): void {\n this.#fetchedAt.clear();\n }\n\n /**\n * Clear freshness for specific keys only, forcing the next fetch to\n * re-request those keys regardless of TTL. Does not affect inflight fetches.\n *\n * @param keys - Keys to mark stale.\n */\n invalidateKeys(keys: Key[]): void {\n for (const key of keys) {\n this.#fetchedAt.delete(key);\n }\n }\n\n /** Clear all freshness and inflight state. */\n destroy(): void {\n this.#fetchedAt.clear();\n this.#inflight.clear();\n }\n\n /**\n * Split keys into those that need a fresh fetch and those already being\n * fetched by another caller. Keys still within the freshness TTL are dropped.\n *\n * @param keys - The keys to classify.\n * @returns `staleKeys` (need fetching) and `inflightKeys` (join existing fetch).\n */\n #partition(keys: Key[]): { staleKeys: Key[]; inflightKeys: Key[] } {\n const now = Date.now();\n const staleKeys: Key[] = [];\n const inflightKeys: Key[] = [];\n\n for (const key of keys) {\n if (this.#isFresh(key, now)) {\n continue;\n }\n if (this.#inflight.has(key)) {\n inflightKeys.push(key);\n } else {\n staleKeys.push(key);\n }\n }\n\n return { staleKeys, inflightKeys };\n }\n\n /**\n * Returns true if the key's last fetch is still within the freshness TTL.\n *\n * @param key - The key to check.\n * @param now - Current timestamp (avoids repeated clock reads).\n * @returns True if the key was fetched within the freshness TTL.\n */\n #isFresh(key: Key, now: number): boolean {\n const fetchedAt = this.#fetchedAt.get(key);\n return fetchedAt !== undefined && now - fetchedAt < this.#freshnessTtlMs;\n }\n\n /**\n * Launch a batch fetch and register a per-key inflight promise for each key\n * so concurrent callers can join. On success, all requested keys are marked\n * fresh — including keys the source returned no value for, since the absence\n * of a value is itself a valid answer that should not be re-asked until the\n * TTL expires. A failed batch leaves keys stale so they are retried.\n *\n * @param staleKeys - Keys to fetch (none of which are already inflight).\n * @returns The batch fetch promise.\n */\n #startBatchFetch(staleKeys: Key[]): Promise<Record<Key, Value>> {\n const batchPromise = this.#fetchBatch(staleKeys).then((values) => {\n const fetchedAt = Date.now();\n for (const key of staleKeys) {\n this.#fetchedAt.set(key, fetchedAt);\n }\n return values;\n });\n\n for (const key of staleKeys) {\n const perKey = batchPromise.then(\n (values) => values[key],\n () => undefined,\n );\n this.#inflight.set(key, perKey);\n }\n\n // Clean up inflight entries once the batch settles (success or failure).\n // Rejection is already surfaced to the caller via the returned batchPromise.\n batchPromise\n .finally(() => {\n for (const key of staleKeys) {\n this.#inflight.delete(key);\n }\n })\n .catch(() => undefined);\n\n return batchPromise;\n }\n\n /**\n * Join the inflight fetches for the given keys and collect their values. Keys\n * whose inflight fetch produced no value are omitted.\n *\n * @param inflightKeys - Keys whose fetches are already in progress.\n * @returns Values keyed by key.\n */\n async #joinInflight(inflightKeys: Key[]): Promise<Record<Key, Value>> {\n const values = {} as Record<Key, Value>;\n\n const results = await Promise.all(\n inflightKeys.map(async (key) => {\n const value = await this.#inflight.get(key);\n return [key, value] as const;\n }),\n );\n\n for (const [key, value] of results) {\n if (value !== undefined) {\n values[key] = value;\n }\n }\n\n return values;\n }\n}\n"]}
@@ -59,6 +59,13 @@ export declare class DedupingBatchFetcher<Key extends string, Value> {
59
59
  * regardless of TTL. Does not affect inflight fetches.
60
60
  */
61
61
  invalidate(): void;
62
+ /**
63
+ * Clear freshness for specific keys only, forcing the next fetch to
64
+ * re-request those keys regardless of TTL. Does not affect inflight fetches.
65
+ *
66
+ * @param keys - Keys to mark stale.
67
+ */
68
+ invalidateKeys(keys: Key[]): void;
62
69
  /** Clear all freshness and inflight state. */
63
70
  destroy(): void;
64
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dedupingBatchFetcher.d.cts","sourceRoot":"","sources":["../../src/utils/dedupingBatchFetcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,IAAI,CACpD,IAAI,EAAE,GAAG,EAAE,KACR,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAEjC,MAAM,MAAM,2BAA2B,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,IAAI;IACnE,sEAAsE;IACtE,UAAU,EAAE,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,oBAAoB,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK;;gBAc7C,OAAO,EAAE,2BAA2B,CAAC,GAAG,EAAE,KAAK,CAAC;IAK5D;;;;OAIG;IACH,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,cAAc,CAAC,EAAE,EAAE,MAAM,EAE5B;IAED;;;;;;;OAOG;IACG,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAmBrD;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB,8CAA8C;IAC9C,OAAO,IAAI,IAAI;CA4GhB"}
1
+ {"version":3,"file":"dedupingBatchFetcher.d.cts","sourceRoot":"","sources":["../../src/utils/dedupingBatchFetcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,IAAI,CACpD,IAAI,EAAE,GAAG,EAAE,KACR,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAEjC,MAAM,MAAM,2BAA2B,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,IAAI;IACnE,sEAAsE;IACtE,UAAU,EAAE,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,oBAAoB,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK;;gBAc7C,OAAO,EAAE,2BAA2B,CAAC,GAAG,EAAE,KAAK,CAAC;IAK5D;;;;OAIG;IACH,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,cAAc,CAAC,EAAE,EAAE,MAAM,EAE5B;IAED;;;;;;;OAOG;IACG,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAmBrD;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB;;;;;OAKG;IACH,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAMjC,8CAA8C;IAC9C,OAAO,IAAI,IAAI;CA4GhB"}
@@ -59,6 +59,13 @@ export declare class DedupingBatchFetcher<Key extends string, Value> {
59
59
  * regardless of TTL. Does not affect inflight fetches.
60
60
  */
61
61
  invalidate(): void;
62
+ /**
63
+ * Clear freshness for specific keys only, forcing the next fetch to
64
+ * re-request those keys regardless of TTL. Does not affect inflight fetches.
65
+ *
66
+ * @param keys - Keys to mark stale.
67
+ */
68
+ invalidateKeys(keys: Key[]): void;
62
69
  /** Clear all freshness and inflight state. */
63
70
  destroy(): void;
64
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dedupingBatchFetcher.d.mts","sourceRoot":"","sources":["../../src/utils/dedupingBatchFetcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,IAAI,CACpD,IAAI,EAAE,GAAG,EAAE,KACR,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAEjC,MAAM,MAAM,2BAA2B,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,IAAI;IACnE,sEAAsE;IACtE,UAAU,EAAE,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,oBAAoB,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK;;gBAc7C,OAAO,EAAE,2BAA2B,CAAC,GAAG,EAAE,KAAK,CAAC;IAK5D;;;;OAIG;IACH,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,cAAc,CAAC,EAAE,EAAE,MAAM,EAE5B;IAED;;;;;;;OAOG;IACG,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAmBrD;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB,8CAA8C;IAC9C,OAAO,IAAI,IAAI;CA4GhB"}
1
+ {"version":3,"file":"dedupingBatchFetcher.d.mts","sourceRoot":"","sources":["../../src/utils/dedupingBatchFetcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,IAAI,CACpD,IAAI,EAAE,GAAG,EAAE,KACR,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAEjC,MAAM,MAAM,2BAA2B,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,IAAI;IACnE,sEAAsE;IACtE,UAAU,EAAE,YAAY,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC;;;OAGG;IACH,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,oBAAoB,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK;;gBAc7C,OAAO,EAAE,2BAA2B,CAAC,GAAG,EAAE,KAAK,CAAC;IAK5D;;;;OAIG;IACH,IAAI,cAAc,IAAI,MAAM,CAE3B;IAED,IAAI,cAAc,CAAC,EAAE,EAAE,MAAM,EAE5B;IAED;;;;;;;OAOG;IACG,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAmBrD;;;OAGG;IACH,UAAU,IAAI,IAAI;IAIlB;;;;;OAKG;IACH,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAMjC,8CAA8C;IAC9C,OAAO,IAAI,IAAI;CA4GhB"}
@@ -82,6 +82,17 @@ export class DedupingBatchFetcher {
82
82
  invalidate() {
83
83
  __classPrivateFieldGet(this, _DedupingBatchFetcher_fetchedAt, "f").clear();
84
84
  }
85
+ /**
86
+ * Clear freshness for specific keys only, forcing the next fetch to
87
+ * re-request those keys regardless of TTL. Does not affect inflight fetches.
88
+ *
89
+ * @param keys - Keys to mark stale.
90
+ */
91
+ invalidateKeys(keys) {
92
+ for (const key of keys) {
93
+ __classPrivateFieldGet(this, _DedupingBatchFetcher_fetchedAt, "f").delete(key);
94
+ }
95
+ }
85
96
  /** Clear all freshness and inflight state. */
86
97
  destroy() {
87
98
  __classPrivateFieldGet(this, _DedupingBatchFetcher_fetchedAt, "f").clear();
@@ -1 +1 @@
1
- {"version":3,"file":"dedupingBatchFetcher.mjs","sourceRoot":"","sources":["../../src/utils/dedupingBatchFetcher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Executes the underlying batched fetch for a set of keys. Only keys that have\n * a value need to be present in the returned record; keys the source had no\n * data for are simply omitted (they are still marked fresh — see\n * {@link DedupingBatchFetcher.fetch}).\n *\n * @param keys - The keys to fetch (already filtered to stale, not-inflight keys).\n * @returns The fetched values keyed by key.\n */\nexport type BatchFetchFn<Key extends string, Value> = (\n keys: Key[],\n) => Promise<Record<Key, Value>>;\n\nexport type DedupingBatchFetcherOptions<Key extends string, Value> = {\n /** Performs the actual batched fetch for stale, not-inflight keys. */\n fetchBatch: BatchFetchFn<Key, Value>;\n /**\n * Minimum age (ms) before a key is considered stale and re-fetched. Keys\n * fetched more recently than this are skipped entirely.\n */\n freshnessTtlMs: number;\n};\n\n/**\n * Deduplicates batched fetches by key across two dimensions:\n *\n * 1. **Freshness TTL** — keys fetched within `freshnessTtlMs` are skipped\n * entirely. Note the freshness window only starts once a fetch *completes*,\n * so it does not cover requests that are still in flight.\n * 2. **Inflight coalescing** — if a fetch is already in progress for a key,\n * concurrent callers join the existing promise instead of issuing a new\n * request. This covers the request-in-flight window the freshness TTL\n * structurally cannot.\n *\n * Both layers are per-key, so a call for a partially-overlapping set of keys\n * reuses the fresh/inflight keys and only fetches the genuinely-missing ones.\n *\n * The fetch is batched: all stale, not-inflight keys from a single `fetch()`\n * call are passed to `fetchBatch` together, then split into per-key results so\n * each key can be joined independently.\n */\nexport class DedupingBatchFetcher<Key extends string, Value> {\n readonly #fetchBatch: BatchFetchFn<Key, Value>;\n\n #freshnessTtlMs: number;\n\n /** Tracks the last successful fetch time per key (freshness gating). */\n readonly #fetchedAt = new Map<Key, number>();\n\n /**\n * Per-key inflight fetch promises. Each resolves to the value, or `undefined`\n * if the batch failed or returned no data for that key.\n */\n readonly #inflight = new Map<Key, Promise<Value | undefined>>();\n\n constructor(options: DedupingBatchFetcherOptions<Key, Value>) {\n this.#fetchBatch = options.fetchBatch;\n this.#freshnessTtlMs = options.freshnessTtlMs;\n }\n\n /**\n * Minimum age (ms) before a key is re-fetched.\n *\n * @returns The current freshness TTL in milliseconds.\n */\n get freshnessTtlMs(): number {\n return this.#freshnessTtlMs;\n }\n\n set freshnessTtlMs(ms: number) {\n this.#freshnessTtlMs = ms;\n }\n\n /**\n * Fetch values for the given keys, deduplicating against fresh and inflight\n * fetches.\n *\n * @param keys - The keys to fetch.\n * @returns Values keyed by key. Only contains entries for keys that were\n * actually fetched (or joined from inflight) and had a value.\n */\n async fetch(keys: Key[]): Promise<Record<Key, Value>> {\n const { staleKeys, inflightKeys } = this.#partition(keys);\n\n if (staleKeys.length === 0 && inflightKeys.length === 0) {\n return {} as Record<Key, Value>;\n }\n\n // Start a fetch for stale keys and join any fetches already in progress.\n const batchPromise =\n staleKeys.length > 0 ? this.#startBatchFetch(staleKeys) : undefined;\n const values = await this.#joinInflight(inflightKeys);\n\n if (batchPromise) {\n Object.assign(values, await batchPromise);\n }\n\n return values;\n }\n\n /**\n * Clear the freshness cache, forcing the next fetch to re-request every key\n * regardless of TTL. Does not affect inflight fetches.\n */\n invalidate(): void {\n this.#fetchedAt.clear();\n }\n\n /** Clear all freshness and inflight state. */\n destroy(): void {\n this.#fetchedAt.clear();\n this.#inflight.clear();\n }\n\n /**\n * Split keys into those that need a fresh fetch and those already being\n * fetched by another caller. Keys still within the freshness TTL are dropped.\n *\n * @param keys - The keys to classify.\n * @returns `staleKeys` (need fetching) and `inflightKeys` (join existing fetch).\n */\n #partition(keys: Key[]): { staleKeys: Key[]; inflightKeys: Key[] } {\n const now = Date.now();\n const staleKeys: Key[] = [];\n const inflightKeys: Key[] = [];\n\n for (const key of keys) {\n if (this.#isFresh(key, now)) {\n continue;\n }\n if (this.#inflight.has(key)) {\n inflightKeys.push(key);\n } else {\n staleKeys.push(key);\n }\n }\n\n return { staleKeys, inflightKeys };\n }\n\n /**\n * Returns true if the key's last fetch is still within the freshness TTL.\n *\n * @param key - The key to check.\n * @param now - Current timestamp (avoids repeated clock reads).\n * @returns True if the key was fetched within the freshness TTL.\n */\n #isFresh(key: Key, now: number): boolean {\n const fetchedAt = this.#fetchedAt.get(key);\n return fetchedAt !== undefined && now - fetchedAt < this.#freshnessTtlMs;\n }\n\n /**\n * Launch a batch fetch and register a per-key inflight promise for each key\n * so concurrent callers can join. On success, all requested keys are marked\n * fresh — including keys the source returned no value for, since the absence\n * of a value is itself a valid answer that should not be re-asked until the\n * TTL expires. A failed batch leaves keys stale so they are retried.\n *\n * @param staleKeys - Keys to fetch (none of which are already inflight).\n * @returns The batch fetch promise.\n */\n #startBatchFetch(staleKeys: Key[]): Promise<Record<Key, Value>> {\n const batchPromise = this.#fetchBatch(staleKeys).then((values) => {\n const fetchedAt = Date.now();\n for (const key of staleKeys) {\n this.#fetchedAt.set(key, fetchedAt);\n }\n return values;\n });\n\n for (const key of staleKeys) {\n const perKey = batchPromise.then(\n (values) => values[key],\n () => undefined,\n );\n this.#inflight.set(key, perKey);\n }\n\n // Clean up inflight entries once the batch settles (success or failure).\n // Rejection is already surfaced to the caller via the returned batchPromise.\n batchPromise\n .finally(() => {\n for (const key of staleKeys) {\n this.#inflight.delete(key);\n }\n })\n .catch(() => undefined);\n\n return batchPromise;\n }\n\n /**\n * Join the inflight fetches for the given keys and collect their values. Keys\n * whose inflight fetch produced no value are omitted.\n *\n * @param inflightKeys - Keys whose fetches are already in progress.\n * @returns Values keyed by key.\n */\n async #joinInflight(inflightKeys: Key[]): Promise<Record<Key, Value>> {\n const values = {} as Record<Key, Value>;\n\n const results = await Promise.all(\n inflightKeys.map(async (key) => {\n const value = await this.#inflight.get(key);\n return [key, value] as const;\n }),\n );\n\n for (const [key, value] of results) {\n if (value !== undefined) {\n values[key] = value;\n }\n }\n\n return values;\n }\n}\n"]}
1
+ {"version":3,"file":"dedupingBatchFetcher.mjs","sourceRoot":"","sources":["../../src/utils/dedupingBatchFetcher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Executes the underlying batched fetch for a set of keys. Only keys that have\n * a value need to be present in the returned record; keys the source had no\n * data for are simply omitted (they are still marked fresh — see\n * {@link DedupingBatchFetcher.fetch}).\n *\n * @param keys - The keys to fetch (already filtered to stale, not-inflight keys).\n * @returns The fetched values keyed by key.\n */\nexport type BatchFetchFn<Key extends string, Value> = (\n keys: Key[],\n) => Promise<Record<Key, Value>>;\n\nexport type DedupingBatchFetcherOptions<Key extends string, Value> = {\n /** Performs the actual batched fetch for stale, not-inflight keys. */\n fetchBatch: BatchFetchFn<Key, Value>;\n /**\n * Minimum age (ms) before a key is considered stale and re-fetched. Keys\n * fetched more recently than this are skipped entirely.\n */\n freshnessTtlMs: number;\n};\n\n/**\n * Deduplicates batched fetches by key across two dimensions:\n *\n * 1. **Freshness TTL** — keys fetched within `freshnessTtlMs` are skipped\n * entirely. Note the freshness window only starts once a fetch *completes*,\n * so it does not cover requests that are still in flight.\n * 2. **Inflight coalescing** — if a fetch is already in progress for a key,\n * concurrent callers join the existing promise instead of issuing a new\n * request. This covers the request-in-flight window the freshness TTL\n * structurally cannot.\n *\n * Both layers are per-key, so a call for a partially-overlapping set of keys\n * reuses the fresh/inflight keys and only fetches the genuinely-missing ones.\n *\n * The fetch is batched: all stale, not-inflight keys from a single `fetch()`\n * call are passed to `fetchBatch` together, then split into per-key results so\n * each key can be joined independently.\n */\nexport class DedupingBatchFetcher<Key extends string, Value> {\n readonly #fetchBatch: BatchFetchFn<Key, Value>;\n\n #freshnessTtlMs: number;\n\n /** Tracks the last successful fetch time per key (freshness gating). */\n readonly #fetchedAt = new Map<Key, number>();\n\n /**\n * Per-key inflight fetch promises. Each resolves to the value, or `undefined`\n * if the batch failed or returned no data for that key.\n */\n readonly #inflight = new Map<Key, Promise<Value | undefined>>();\n\n constructor(options: DedupingBatchFetcherOptions<Key, Value>) {\n this.#fetchBatch = options.fetchBatch;\n this.#freshnessTtlMs = options.freshnessTtlMs;\n }\n\n /**\n * Minimum age (ms) before a key is re-fetched.\n *\n * @returns The current freshness TTL in milliseconds.\n */\n get freshnessTtlMs(): number {\n return this.#freshnessTtlMs;\n }\n\n set freshnessTtlMs(ms: number) {\n this.#freshnessTtlMs = ms;\n }\n\n /**\n * Fetch values for the given keys, deduplicating against fresh and inflight\n * fetches.\n *\n * @param keys - The keys to fetch.\n * @returns Values keyed by key. Only contains entries for keys that were\n * actually fetched (or joined from inflight) and had a value.\n */\n async fetch(keys: Key[]): Promise<Record<Key, Value>> {\n const { staleKeys, inflightKeys } = this.#partition(keys);\n\n if (staleKeys.length === 0 && inflightKeys.length === 0) {\n return {} as Record<Key, Value>;\n }\n\n // Start a fetch for stale keys and join any fetches already in progress.\n const batchPromise =\n staleKeys.length > 0 ? this.#startBatchFetch(staleKeys) : undefined;\n const values = await this.#joinInflight(inflightKeys);\n\n if (batchPromise) {\n Object.assign(values, await batchPromise);\n }\n\n return values;\n }\n\n /**\n * Clear the freshness cache, forcing the next fetch to re-request every key\n * regardless of TTL. Does not affect inflight fetches.\n */\n invalidate(): void {\n this.#fetchedAt.clear();\n }\n\n /**\n * Clear freshness for specific keys only, forcing the next fetch to\n * re-request those keys regardless of TTL. Does not affect inflight fetches.\n *\n * @param keys - Keys to mark stale.\n */\n invalidateKeys(keys: Key[]): void {\n for (const key of keys) {\n this.#fetchedAt.delete(key);\n }\n }\n\n /** Clear all freshness and inflight state. */\n destroy(): void {\n this.#fetchedAt.clear();\n this.#inflight.clear();\n }\n\n /**\n * Split keys into those that need a fresh fetch and those already being\n * fetched by another caller. Keys still within the freshness TTL are dropped.\n *\n * @param keys - The keys to classify.\n * @returns `staleKeys` (need fetching) and `inflightKeys` (join existing fetch).\n */\n #partition(keys: Key[]): { staleKeys: Key[]; inflightKeys: Key[] } {\n const now = Date.now();\n const staleKeys: Key[] = [];\n const inflightKeys: Key[] = [];\n\n for (const key of keys) {\n if (this.#isFresh(key, now)) {\n continue;\n }\n if (this.#inflight.has(key)) {\n inflightKeys.push(key);\n } else {\n staleKeys.push(key);\n }\n }\n\n return { staleKeys, inflightKeys };\n }\n\n /**\n * Returns true if the key's last fetch is still within the freshness TTL.\n *\n * @param key - The key to check.\n * @param now - Current timestamp (avoids repeated clock reads).\n * @returns True if the key was fetched within the freshness TTL.\n */\n #isFresh(key: Key, now: number): boolean {\n const fetchedAt = this.#fetchedAt.get(key);\n return fetchedAt !== undefined && now - fetchedAt < this.#freshnessTtlMs;\n }\n\n /**\n * Launch a batch fetch and register a per-key inflight promise for each key\n * so concurrent callers can join. On success, all requested keys are marked\n * fresh — including keys the source returned no value for, since the absence\n * of a value is itself a valid answer that should not be re-asked until the\n * TTL expires. A failed batch leaves keys stale so they are retried.\n *\n * @param staleKeys - Keys to fetch (none of which are already inflight).\n * @returns The batch fetch promise.\n */\n #startBatchFetch(staleKeys: Key[]): Promise<Record<Key, Value>> {\n const batchPromise = this.#fetchBatch(staleKeys).then((values) => {\n const fetchedAt = Date.now();\n for (const key of staleKeys) {\n this.#fetchedAt.set(key, fetchedAt);\n }\n return values;\n });\n\n for (const key of staleKeys) {\n const perKey = batchPromise.then(\n (values) => values[key],\n () => undefined,\n );\n this.#inflight.set(key, perKey);\n }\n\n // Clean up inflight entries once the batch settles (success or failure).\n // Rejection is already surfaced to the caller via the returned batchPromise.\n batchPromise\n .finally(() => {\n for (const key of staleKeys) {\n this.#inflight.delete(key);\n }\n })\n .catch(() => undefined);\n\n return batchPromise;\n }\n\n /**\n * Join the inflight fetches for the given keys and collect their values. Keys\n * whose inflight fetch produced no value are omitted.\n *\n * @param inflightKeys - Keys whose fetches are already in progress.\n * @returns Values keyed by key.\n */\n async #joinInflight(inflightKeys: Key[]): Promise<Record<Key, Value>> {\n const values = {} as Record<Key, Value>;\n\n const results = await Promise.all(\n inflightKeys.map(async (key) => {\n const value = await this.#inflight.get(key);\n return [key, value] as const;\n }),\n );\n\n for (const [key, value] of results) {\n if (value !== undefined) {\n values[key] = value;\n }\n }\n\n return values;\n }\n}\n"]}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.processAccountActivityBalanceUpdates = void 0;
7
+ const bignumber_js_1 = __importDefault(require("bignumber.js"));
8
+ /**
9
+ * Convert AccountActivityMessage balance updates into a {@link DataResponse}
10
+ * for AssetsController (same shape as BackendWebsocketDataSource).
11
+ *
12
+ * @param updates - Balance updates from account-activity websocket payload.
13
+ * @param accountId - Internal account UUID.
14
+ * @param getAssetType - Resolver for asset metadata type.
15
+ * @returns DataResponse with merge mode when balances are present.
16
+ */
17
+ function processAccountActivityBalanceUpdates(updates, accountId, getAssetType) {
18
+ const assetsBalance = Object.create(null);
19
+ assetsBalance[accountId] = Object.create(null);
20
+ const assetsMetadata = Object.create(null);
21
+ for (const update of updates) {
22
+ const { asset, postBalance } = update;
23
+ if (!asset || !postBalance) {
24
+ continue;
25
+ }
26
+ const assetId = asset.type;
27
+ if (asset.decimals === undefined) {
28
+ continue;
29
+ }
30
+ const rawBalanceStr = postBalance.amount.startsWith('0x')
31
+ ? BigInt(postBalance.amount).toString()
32
+ : postBalance.amount;
33
+ const humanReadableAmount = new bignumber_js_1.default(rawBalanceStr)
34
+ .dividedBy(new bignumber_js_1.default(10).pow(asset.decimals))
35
+ .toFixed();
36
+ assetsBalance[accountId][assetId] = {
37
+ amount: humanReadableAmount,
38
+ };
39
+ assetsMetadata[assetId] = {
40
+ type: getAssetType(assetId),
41
+ symbol: asset.unit,
42
+ name: asset.unit,
43
+ decimals: asset.decimals,
44
+ };
45
+ }
46
+ const response = { updateMode: 'merge' };
47
+ if (Object.keys(assetsBalance[accountId]).length > 0) {
48
+ response.assetsBalance = assetsBalance;
49
+ response.assetsInfo = assetsMetadata;
50
+ }
51
+ return response;
52
+ }
53
+ exports.processAccountActivityBalanceUpdates = processAccountActivityBalanceUpdates;
54
+ //# sourceMappingURL=processAccountActivityBalanceUpdates.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processAccountActivityBalanceUpdates.cjs","sourceRoot":"","sources":["../../src/utils/processAccountActivityBalanceUpdates.ts"],"names":[],"mappings":";;;;;;AACA,gEAAuC;AASvC;;;;;;;;GAQG;AACH,SAAgB,oCAAoC,CAClD,OAAwB,EACxB,SAAiB,EACjB,YAAoE;IAEpE,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAGvC,CAAC;IACF,aAAa,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAG5C,CAAC;IACF,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAGxC,CAAC;IAEF,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;QAEtC,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;YAC3B,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAqB,CAAC;QAE5C,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACjC,SAAS;QACX,CAAC;QAED,MAAM,aAAa,GAAG,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;YACvD,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;YACvC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC;QAEvB,MAAM,mBAAmB,GAAG,IAAI,sBAAW,CAAC,aAAa,CAAC;aACvD,SAAS,CAAC,IAAI,sBAAW,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;aAClD,OAAO,EAAE,CAAC;QAEb,aAAa,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,GAAG;YAClC,MAAM,EAAE,mBAAmB;SAC5B,CAAC;QAEF,cAAc,CAAC,OAAO,CAAC,GAAG;YACxB,IAAI,EAAE,YAAY,CAAC,OAAO,CAAC;YAC3B,MAAM,EAAE,KAAK,CAAC,IAAI;YAClB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACzB,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAiB,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;IACvD,IAAI,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrD,QAAQ,CAAC,aAAa,GAAG,aAAa,CAAC;QACvC,QAAQ,CAAC,UAAU,GAAG,cAAc,CAAC;IACvC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AA1DD,oFA0DC","sourcesContent":["import type { BalanceUpdate } from '@metamask/core-backend';\nimport BigNumberJS from 'bignumber.js';\n\nimport type {\n AssetBalance,\n AssetMetadata,\n Caip19AssetId,\n DataResponse,\n} from '../types';\n\n/**\n * Convert AccountActivityMessage balance updates into a {@link DataResponse}\n * for AssetsController (same shape as BackendWebsocketDataSource).\n *\n * @param updates - Balance updates from account-activity websocket payload.\n * @param accountId - Internal account UUID.\n * @param getAssetType - Resolver for asset metadata type.\n * @returns DataResponse with merge mode when balances are present.\n */\nexport function processAccountActivityBalanceUpdates(\n updates: BalanceUpdate[],\n accountId: string,\n getAssetType: (assetId: Caip19AssetId) => 'native' | 'erc20' | 'spl',\n): DataResponse {\n const assetsBalance = Object.create(null) as Record<\n string,\n Record<Caip19AssetId, AssetBalance>\n >;\n assetsBalance[accountId] = Object.create(null) as Record<\n Caip19AssetId,\n AssetBalance\n >;\n const assetsMetadata = Object.create(null) as Record<\n Caip19AssetId,\n AssetMetadata\n >;\n\n for (const update of updates) {\n const { asset, postBalance } = update;\n\n if (!asset || !postBalance) {\n continue;\n }\n\n const assetId = asset.type as Caip19AssetId;\n\n if (asset.decimals === undefined) {\n continue;\n }\n\n const rawBalanceStr = postBalance.amount.startsWith('0x')\n ? BigInt(postBalance.amount).toString()\n : postBalance.amount;\n\n const humanReadableAmount = new BigNumberJS(rawBalanceStr)\n .dividedBy(new BigNumberJS(10).pow(asset.decimals))\n .toFixed();\n\n assetsBalance[accountId][assetId] = {\n amount: humanReadableAmount,\n };\n\n assetsMetadata[assetId] = {\n type: getAssetType(assetId),\n symbol: asset.unit,\n name: asset.unit,\n decimals: asset.decimals,\n };\n }\n\n const response: DataResponse = { updateMode: 'merge' };\n if (Object.keys(assetsBalance[accountId]).length > 0) {\n response.assetsBalance = assetsBalance;\n response.assetsInfo = assetsMetadata;\n }\n\n return response;\n}\n"]}
@@ -0,0 +1,13 @@
1
+ import type { BalanceUpdate } from "@metamask/core-backend";
2
+ import type { Caip19AssetId, DataResponse } from "../types.cjs";
3
+ /**
4
+ * Convert AccountActivityMessage balance updates into a {@link DataResponse}
5
+ * for AssetsController (same shape as BackendWebsocketDataSource).
6
+ *
7
+ * @param updates - Balance updates from account-activity websocket payload.
8
+ * @param accountId - Internal account UUID.
9
+ * @param getAssetType - Resolver for asset metadata type.
10
+ * @returns DataResponse with merge mode when balances are present.
11
+ */
12
+ export declare function processAccountActivityBalanceUpdates(updates: BalanceUpdate[], accountId: string, getAssetType: (assetId: Caip19AssetId) => 'native' | 'erc20' | 'spl'): DataResponse;
13
+ //# sourceMappingURL=processAccountActivityBalanceUpdates.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processAccountActivityBalanceUpdates.d.cts","sourceRoot":"","sources":["../../src/utils/processAccountActivityBalanceUpdates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,+BAA+B;AAG5D,OAAO,KAAK,EAGV,aAAa,EACb,YAAY,EACb,qBAAiB;AAElB;;;;;;;;GAQG;AACH,wBAAgB,oCAAoC,CAClD,OAAO,EAAE,aAAa,EAAE,EACxB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,QAAQ,GAAG,OAAO,GAAG,KAAK,GACnE,YAAY,CAsDd"}
@@ -0,0 +1,13 @@
1
+ import type { BalanceUpdate } from "@metamask/core-backend";
2
+ import type { Caip19AssetId, DataResponse } from "../types.mjs";
3
+ /**
4
+ * Convert AccountActivityMessage balance updates into a {@link DataResponse}
5
+ * for AssetsController (same shape as BackendWebsocketDataSource).
6
+ *
7
+ * @param updates - Balance updates from account-activity websocket payload.
8
+ * @param accountId - Internal account UUID.
9
+ * @param getAssetType - Resolver for asset metadata type.
10
+ * @returns DataResponse with merge mode when balances are present.
11
+ */
12
+ export declare function processAccountActivityBalanceUpdates(updates: BalanceUpdate[], accountId: string, getAssetType: (assetId: Caip19AssetId) => 'native' | 'erc20' | 'spl'): DataResponse;
13
+ //# sourceMappingURL=processAccountActivityBalanceUpdates.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processAccountActivityBalanceUpdates.d.mts","sourceRoot":"","sources":["../../src/utils/processAccountActivityBalanceUpdates.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,+BAA+B;AAG5D,OAAO,KAAK,EAGV,aAAa,EACb,YAAY,EACb,qBAAiB;AAElB;;;;;;;;GAQG;AACH,wBAAgB,oCAAoC,CAClD,OAAO,EAAE,aAAa,EAAE,EACxB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,QAAQ,GAAG,OAAO,GAAG,KAAK,GACnE,YAAY,CAsDd"}
@@ -0,0 +1,47 @@
1
+ import BigNumberJS from "bignumber.js";
2
+ /**
3
+ * Convert AccountActivityMessage balance updates into a {@link DataResponse}
4
+ * for AssetsController (same shape as BackendWebsocketDataSource).
5
+ *
6
+ * @param updates - Balance updates from account-activity websocket payload.
7
+ * @param accountId - Internal account UUID.
8
+ * @param getAssetType - Resolver for asset metadata type.
9
+ * @returns DataResponse with merge mode when balances are present.
10
+ */
11
+ export function processAccountActivityBalanceUpdates(updates, accountId, getAssetType) {
12
+ const assetsBalance = Object.create(null);
13
+ assetsBalance[accountId] = Object.create(null);
14
+ const assetsMetadata = Object.create(null);
15
+ for (const update of updates) {
16
+ const { asset, postBalance } = update;
17
+ if (!asset || !postBalance) {
18
+ continue;
19
+ }
20
+ const assetId = asset.type;
21
+ if (asset.decimals === undefined) {
22
+ continue;
23
+ }
24
+ const rawBalanceStr = postBalance.amount.startsWith('0x')
25
+ ? BigInt(postBalance.amount).toString()
26
+ : postBalance.amount;
27
+ const humanReadableAmount = new BigNumberJS(rawBalanceStr)
28
+ .dividedBy(new BigNumberJS(10).pow(asset.decimals))
29
+ .toFixed();
30
+ assetsBalance[accountId][assetId] = {
31
+ amount: humanReadableAmount,
32
+ };
33
+ assetsMetadata[assetId] = {
34
+ type: getAssetType(assetId),
35
+ symbol: asset.unit,
36
+ name: asset.unit,
37
+ decimals: asset.decimals,
38
+ };
39
+ }
40
+ const response = { updateMode: 'merge' };
41
+ if (Object.keys(assetsBalance[accountId]).length > 0) {
42
+ response.assetsBalance = assetsBalance;
43
+ response.assetsInfo = assetsMetadata;
44
+ }
45
+ return response;
46
+ }
47
+ //# sourceMappingURL=processAccountActivityBalanceUpdates.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processAccountActivityBalanceUpdates.mjs","sourceRoot":"","sources":["../../src/utils/processAccountActivityBalanceUpdates.ts"],"names":[],"mappings":"AACA,OAAO,WAAW,qBAAqB;AASvC;;;;;;;;GAQG;AACH,MAAM,UAAU,oCAAoC,CAClD,OAAwB,EACxB,SAAiB,EACjB,YAAoE;IAEpE,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAGvC,CAAC;IACF,aAAa,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAG5C,CAAC;IACF,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAGxC,CAAC;IAEF,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC;QAEtC,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;YAC3B,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,KAAK,CAAC,IAAqB,CAAC;QAE5C,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACjC,SAAS;QACX,CAAC;QAED,MAAM,aAAa,GAAG,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;YACvD,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE;YACvC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC;QAEvB,MAAM,mBAAmB,GAAG,IAAI,WAAW,CAAC,aAAa,CAAC;aACvD,SAAS,CAAC,IAAI,WAAW,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;aAClD,OAAO,EAAE,CAAC;QAEb,aAAa,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,GAAG;YAClC,MAAM,EAAE,mBAAmB;SAC5B,CAAC;QAEF,cAAc,CAAC,OAAO,CAAC,GAAG;YACxB,IAAI,EAAE,YAAY,CAAC,OAAO,CAAC;YAC3B,MAAM,EAAE,KAAK,CAAC,IAAI;YAClB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACzB,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAiB,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;IACvD,IAAI,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrD,QAAQ,CAAC,aAAa,GAAG,aAAa,CAAC;QACvC,QAAQ,CAAC,UAAU,GAAG,cAAc,CAAC;IACvC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["import type { BalanceUpdate } from '@metamask/core-backend';\nimport BigNumberJS from 'bignumber.js';\n\nimport type {\n AssetBalance,\n AssetMetadata,\n Caip19AssetId,\n DataResponse,\n} from '../types';\n\n/**\n * Convert AccountActivityMessage balance updates into a {@link DataResponse}\n * for AssetsController (same shape as BackendWebsocketDataSource).\n *\n * @param updates - Balance updates from account-activity websocket payload.\n * @param accountId - Internal account UUID.\n * @param getAssetType - Resolver for asset metadata type.\n * @returns DataResponse with merge mode when balances are present.\n */\nexport function processAccountActivityBalanceUpdates(\n updates: BalanceUpdate[],\n accountId: string,\n getAssetType: (assetId: Caip19AssetId) => 'native' | 'erc20' | 'spl',\n): DataResponse {\n const assetsBalance = Object.create(null) as Record<\n string,\n Record<Caip19AssetId, AssetBalance>\n >;\n assetsBalance[accountId] = Object.create(null) as Record<\n Caip19AssetId,\n AssetBalance\n >;\n const assetsMetadata = Object.create(null) as Record<\n Caip19AssetId,\n AssetMetadata\n >;\n\n for (const update of updates) {\n const { asset, postBalance } = update;\n\n if (!asset || !postBalance) {\n continue;\n }\n\n const assetId = asset.type as Caip19AssetId;\n\n if (asset.decimals === undefined) {\n continue;\n }\n\n const rawBalanceStr = postBalance.amount.startsWith('0x')\n ? BigInt(postBalance.amount).toString()\n : postBalance.amount;\n\n const humanReadableAmount = new BigNumberJS(rawBalanceStr)\n .dividedBy(new BigNumberJS(10).pow(asset.decimals))\n .toFixed();\n\n assetsBalance[accountId][assetId] = {\n amount: humanReadableAmount,\n };\n\n assetsMetadata[assetId] = {\n type: getAssetType(assetId),\n symbol: asset.unit,\n name: asset.unit,\n decimals: asset.decimals,\n };\n }\n\n const response: DataResponse = { updateMode: 'merge' };\n if (Object.keys(assetsBalance[accountId]).length > 0) {\n response.assetsBalance = assetsBalance;\n response.assetsInfo = assetsMetadata;\n }\n\n return response;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metamask/assets-controller",
3
- "version": "9.1.0",
3
+ "version": "10.0.0",
4
4
  "description": "Tracks assets balances/prices and handles token detection across all digital assets",
5
5
  "keywords": [
6
6
  "Ethereum",
@@ -62,7 +62,7 @@
62
62
  "@metamask/base-controller": "^9.1.0",
63
63
  "@metamask/client-controller": "^1.0.1",
64
64
  "@metamask/controller-utils": "^12.3.0",
65
- "@metamask/core-backend": "^6.3.3",
65
+ "@metamask/core-backend": "^6.4.0",
66
66
  "@metamask/keyring-api": "^23.3.0",
67
67
  "@metamask/keyring-controller": "^27.1.0",
68
68
  "@metamask/keyring-internal-api": "^11.0.1",
@@ -76,7 +76,7 @@
76
76
  "@metamask/preferences-controller": "^23.1.0",
77
77
  "@metamask/snaps-controllers": "^19.0.0",
78
78
  "@metamask/snaps-utils": "^12.1.2",
79
- "@metamask/transaction-controller": "^68.1.1",
79
+ "@metamask/transaction-controller": "^68.2.0",
80
80
  "@metamask/utils": "^11.11.0",
81
81
  "async-mutex": "^0.5.0",
82
82
  "bignumber.js": "^9.1.2",