{"version":3,"file":"EarnController.mjs","sourceRoot":"","sources":["../src/EarnController.ts"],"names":[],"mappings":"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","sourcesContent":["import { Web3Provider } from '@ethersproject/providers';\nimport type {\n  AccountsControllerGetSelectedAccountAction,\n  AccountsControllerSelectedAccountChangeEvent,\n} from '@metamask/accounts-controller';\nimport type {\n  ControllerGetStateAction,\n  ControllerStateChangeEvent,\n  RestrictedControllerMessenger,\n  StateMetadata,\n} from '@metamask/base-controller';\nimport { BaseController } from '@metamask/base-controller';\nimport { convertHexToDecimal } from '@metamask/controller-utils';\nimport type { NetworkControllerStateChangeEvent } from '@metamask/network-controller';\nimport type {\n  NetworkControllerGetNetworkClientByIdAction,\n  NetworkControllerGetStateAction,\n} from '@metamask/network-controller';\nimport {\n  StakeSdk,\n  StakingApiService,\n  type PooledStake,\n  type StakeSdkConfig,\n  type VaultData,\n} from '@metamask/stake-sdk';\n\nexport const controllerName = 'EarnController';\n\nexport type PooledStakingState = {\n  pooledStakes: PooledStake;\n  exchangeRate: string;\n  vaultData: VaultData;\n  isEligible: boolean;\n};\n\nexport type StablecoinLendingState = {\n  vaults: StablecoinVault[];\n};\n\nexport type StablecoinVault = {\n  symbol: string;\n  name: string;\n  chainId: number;\n  tokenAddress: string;\n  vaultAddress: string;\n  currentAPY: string;\n  supply: string;\n  liquidity: string;\n};\n\n/**\n * Metadata for the EarnController.\n */\nconst earnControllerMetadata: StateMetadata<EarnControllerState> = {\n  pooled_staking: {\n    persist: true,\n    anonymous: false,\n  },\n  stablecoin_lending: {\n    persist: true,\n    anonymous: false,\n  },\n  lastUpdated: {\n    persist: false,\n    anonymous: true,\n  },\n};\n\n// === State Types ===\nexport type EarnControllerState = {\n  pooled_staking: PooledStakingState;\n  stablecoin_lending?: StablecoinLendingState;\n  lastUpdated: number;\n};\n\n// === Default State ===\nconst DEFAULT_STABLECOIN_VAULT: StablecoinVault = {\n  symbol: '',\n  name: '',\n  chainId: 0,\n  tokenAddress: '',\n  vaultAddress: '',\n  currentAPY: '0',\n  supply: '0',\n  liquidity: '0',\n};\n\n/**\n * Gets the default state for the EarnController.\n *\n * @returns The default EarnController state.\n */\nexport function getDefaultEarnControllerState(): EarnControllerState {\n  return {\n    pooled_staking: {\n      pooledStakes: {\n        account: '',\n        lifetimeRewards: '0',\n        assets: '0',\n        exitRequests: [],\n      },\n      exchangeRate: '1',\n      vaultData: {\n        apy: '0',\n        capacity: '0',\n        feePercent: 0,\n        totalAssets: '0',\n        vaultAddress: '0x0000000000000000000000000000000000000000',\n      },\n      isEligible: false,\n    },\n    stablecoin_lending: {\n      vaults: [DEFAULT_STABLECOIN_VAULT],\n    },\n    lastUpdated: 0,\n  };\n}\n\n// === MESSENGER ===\n\n/**\n * The action which can be used to retrieve the state of the EarnController.\n */\nexport type EarnControllerGetStateAction = ControllerGetStateAction<\n  typeof controllerName,\n  EarnControllerState\n>;\n\n/**\n * All actions that EarnController registers, to be called externally.\n */\nexport type EarnControllerActions = EarnControllerGetStateAction;\n\n/**\n * All actions that EarnController calls internally.\n */\nexport type AllowedActions =\n  | NetworkControllerGetNetworkClientByIdAction\n  | NetworkControllerGetStateAction\n  | AccountsControllerGetSelectedAccountAction;\n\n/**\n * The event that EarnController publishes when updating state.\n */\nexport type EarnControllerStateChangeEvent = ControllerStateChangeEvent<\n  typeof controllerName,\n  EarnControllerState\n>;\n\n/**\n * All events that EarnController publishes, to be subscribed to externally.\n */\nexport type EarnControllerEvents = EarnControllerStateChangeEvent;\n\n/**\n * All events that EarnController subscribes to internally.\n */\nexport type AllowedEvents =\n  | AccountsControllerSelectedAccountChangeEvent\n  | NetworkControllerStateChangeEvent;\n\n/**\n * The messenger which is restricted to actions and events accessed by\n * EarnController.\n */\nexport type EarnControllerMessenger = RestrictedControllerMessenger<\n  typeof controllerName,\n  EarnControllerActions | AllowedActions,\n  EarnControllerEvents | AllowedEvents,\n  AllowedActions['type'],\n  AllowedEvents['type']\n>;\n\n// === CONTROLLER DEFINITION ===\n\n/**\n * EarnController manages DeFi earning opportunities across different protocols and chains.\n */\nexport class EarnController extends BaseController<\n  typeof controllerName,\n  EarnControllerState,\n  EarnControllerMessenger\n> {\n  #stakeSDK: StakeSdk | null = null;\n\n  #selectedNetworkClientId?: string;\n\n  readonly #stakingApiService: StakingApiService = new StakingApiService();\n\n  constructor({\n    messenger,\n    state = {},\n  }: {\n    messenger: EarnControllerMessenger;\n    state?: Partial<EarnControllerState>;\n  }) {\n    super({\n      name: controllerName,\n      metadata: earnControllerMetadata,\n      messenger,\n      state: {\n        ...getDefaultEarnControllerState(),\n        ...state,\n      },\n    });\n\n    this.#initializeSDK();\n    this.refreshPooledStakingData().catch(console.error);\n\n    const { selectedNetworkClientId } = this.messagingSystem.call(\n      'NetworkController:getState',\n    );\n    this.#selectedNetworkClientId = selectedNetworkClientId;\n\n    this.messagingSystem.subscribe(\n      'NetworkController:stateChange',\n      (networkControllerState) => {\n        if (\n          networkControllerState.selectedNetworkClientId !==\n          this.#selectedNetworkClientId\n        ) {\n          this.#initializeSDK(networkControllerState.selectedNetworkClientId);\n          this.refreshPooledStakingData().catch(console.error);\n        }\n        this.#selectedNetworkClientId =\n          networkControllerState.selectedNetworkClientId;\n      },\n    );\n\n    // Listen for account changes\n    this.messagingSystem.subscribe(\n      'AccountsController:selectedAccountChange',\n      () => {\n        this.refreshPooledStakingData().catch(console.error);\n      },\n    );\n  }\n\n  #initializeSDK(networkClientId?: string) {\n    const { selectedNetworkClientId } = networkClientId\n      ? { selectedNetworkClientId: networkClientId }\n      : this.messagingSystem.call('NetworkController:getState');\n\n    const networkClient = this.messagingSystem.call(\n      'NetworkController:getNetworkClientById',\n      selectedNetworkClientId,\n    );\n\n    if (!networkClient?.provider) {\n      this.#stakeSDK = null;\n      return;\n    }\n\n    const provider = new Web3Provider(networkClient.provider);\n    const { chainId } = networkClient.configuration;\n\n    // Initialize appropriate contracts based on chainId\n    const config: StakeSdkConfig = {\n      chainId: convertHexToDecimal(chainId),\n    };\n\n    try {\n      this.#stakeSDK = StakeSdk.create(config);\n      this.#stakeSDK.pooledStakingContract.connectSignerOrProvider(provider);\n    } catch (error) {\n      this.#stakeSDK = null;\n      // Only log unexpected errors, not unsupported chain errors\n      if (\n        !(\n          error instanceof Error &&\n          error.message.includes('Unsupported chainId')\n        )\n      ) {\n        console.error('Stake SDK initialization failed:', error);\n      }\n    }\n  }\n\n  #getCurrentAccount() {\n    return this.messagingSystem.call('AccountsController:getSelectedAccount');\n  }\n\n  #getCurrentChainId(): number {\n    const { selectedNetworkClientId } = this.messagingSystem.call(\n      'NetworkController:getState',\n    );\n    const {\n      configuration: { chainId },\n    } = this.messagingSystem.call(\n      'NetworkController:getNetworkClientById',\n      selectedNetworkClientId,\n    );\n    return convertHexToDecimal(chainId);\n  }\n\n  /**\n   * Refreshes the pooled stakes data for the current account.\n   * Fetches updated stake information including lifetime rewards, assets, and exit requests\n   * from the staking API service and updates the state.\n   *\n   * @returns A promise that resolves when the stakes data has been updated\n   */\n  async refreshPooledStakes(): Promise<void> {\n    const currentAccount = this.#getCurrentAccount();\n    if (!currentAccount?.address) {\n      return;\n    }\n\n    const chainId = this.#getCurrentChainId();\n\n    const { accounts, exchangeRate } =\n      await this.#stakingApiService.getPooledStakes(\n        [currentAccount.address],\n        chainId,\n      );\n\n    this.update((state) => {\n      state.pooled_staking.pooledStakes = accounts[0];\n      state.pooled_staking.exchangeRate = exchangeRate;\n    });\n  }\n\n  /**\n   * Refreshes the staking eligibility status for the current account.\n   * Updates the eligibility status in the controller state based on the location and address blocklist for compliance.\n   *\n   * @returns A promise that resolves when the eligibility status has been updated\n   */\n  async refreshStakingEligibility(): Promise<void> {\n    const currentAccount = this.#getCurrentAccount();\n    if (!currentAccount?.address) {\n      return;\n    }\n\n    const { eligible: isEligible } =\n      await this.#stakingApiService.getPooledStakingEligibility([\n        currentAccount.address,\n      ]);\n\n    this.update((state) => {\n      state.pooled_staking.isEligible = isEligible;\n    });\n  }\n\n  /**\n   * Refreshes vault data for the current chain.\n   * Updates the vault data in the controller state including APY, capacity,\n   * fee percentage, total assets, and vault address.\n   *\n   * @returns A promise that resolves when the vault data has been updated\n   */\n  async refreshVaultData(): Promise<void> {\n    const chainId = this.#getCurrentChainId();\n    const vaultData = await this.#stakingApiService.getVaultData(chainId);\n\n    this.update((state) => {\n      state.pooled_staking.vaultData = vaultData;\n    });\n  }\n\n  /**\n   * Refreshes all pooled staking related data including stakes, eligibility, and vault data.\n   * This method allows partial success, meaning some data may update while other requests fail.\n   * All errors are collected and thrown as a single error message.\n   *\n   * @returns A promise that resolves when all possible data has been updated\n   * @throws {Error} If any of the refresh operations fail, with concatenated error messages\n   */\n  async refreshPooledStakingData(): Promise<void> {\n    const errors: Error[] = [];\n\n    await Promise.all([\n      this.refreshPooledStakes().catch((error) => {\n        errors.push(error);\n      }),\n      this.refreshStakingEligibility().catch((error) => {\n        errors.push(error);\n      }),\n      this.refreshVaultData().catch((error) => {\n        errors.push(error);\n      }),\n    ]);\n\n    if (errors.length > 0) {\n      throw new Error(\n        `Failed to refresh some staking data: ${errors\n          .map((e) => e.message)\n          .join(', ')}`,\n      );\n    }\n  }\n}\n"]}