Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
Expand All @@ -17,6 +17,7 @@ import { MoneyHomeViewTestIds } from './MoneyHomeView.testIds';
import styleSheet from './MoneyHomeView.styles';
import { MUSD_CONVERSION_APY } from '../../../Earn/constants/musd';
import { useMusdConversionTokens } from '../../../Earn/hooks/useMusdConversionTokens';
import useMoneyAccountBalance from '../../hooks/useMoneyAccountBalance';

const Divider = () => <Box twClassName="h-px bg-border-muted my-5" />;

Expand All @@ -27,6 +28,20 @@ const MoneyHomeView = () => {
const navigation = useNavigation();
const { styles } = useStyles(styleSheet, {});

const {
musdBalanceResult,
musdShfvdBalanceResult,
exchangeRateResult,
vaultApyResult,
musdEquivalentBalanceResult,
musdFiatFormatted,
musdSHFvdFiatFormatted,
totalFiatFormatted,
tokenTotal,
} = useMoneyAccountBalance();

console.log('vaultApyResult: ', vaultApyResult);

const { tokens: conversionTokens } = useMusdConversionTokens();

const handleBackPress = useCallback(() => {
Expand Down
177 changes: 177 additions & 0 deletions app/components/UI/Money/hooks/useMoneyAccountBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { useSelector } from 'react-redux';
import { selectSelectedInternalAccountByScope } from '../../../../selectors/multichainAccounts/accounts';
import { EVM_SCOPE } from '../../Earn/constants/networks';
import { useMemo } from 'react';
import {
MoneyAccountBalanceService,
type Erc20BalanceResponse,
type MusdEquivalentValueResponse,
type ExchangeRateResponse,
VaultApyResponse,
} from '@metamask/money-account-controller';
import { useQueries, type UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import useFiatFormatter from '../../SimulationDetails/FiatDisplay/useFiatFormatter';
import { selectTokenMarketData } from '../../../../selectors/tokenRatesController';
import { selectCurrencyRates } from '../../../../selectors/currencyRateController';
import { selectNetworkConfigurations } from '../../../../selectors/networkController';
import {
MUSD_TOKEN_ADDRESS_BY_CHAIN,
MUSD_DECIMALS,
} from '../../Earn/constants/musd';
import { fromTokenMinimalUnitString } from '../../../../util/number';
import { toChecksumAddress } from '../../../../util/address';

// Placeholder hook to unblock work needing balances.
const useMoneyAccountBalance = () => {
// TODO: Replace with selector for actual money account.
const selectedEvmAddress = useSelector(selectSelectedInternalAccountByScope)(
EVM_SCOPE,
)?.address;

const tokenMarketData = useSelector(selectTokenMarketData);
const currencyRates = useSelector(selectCurrencyRates);
const networkConfigurations = useSelector(selectNetworkConfigurations);
const formatFiat = useFiatFormatter();

// Query Key Factory.
const queryKeys = useMemo(
() => ({
GET_MUSD_BALANCE: [
`${MoneyAccountBalanceService.name}:getMusdBalance`,
selectedEvmAddress,
],
GET_MUSDSHFVD_BALANCE: [
`${MoneyAccountBalanceService.name}:getMusdSHFvdBalance`,
selectedEvmAddress,
],
GET_EXCHANGE_RATE: [`${MoneyAccountBalanceService.name}:getExchangeRate`],
GET_VAULT_APY: [`${MoneyAccountBalanceService.name}:getVaultApy`],
GET_MUSD_EQUIVALENT_VALUE: [
`${MoneyAccountBalanceService.name}:getMusdEquivalentValue`,
selectedEvmAddress,
],
}),
[selectedEvmAddress],
);

const [
musdBalanceResult,
musdShfvdBalanceResult,
exchangeRateResult,
vaultApyResult,
musdEquivalentBalanceResult,
] = useQueries({
queries: [
{
queryKey: queryKeys.GET_MUSD_BALANCE,
enabled: Boolean(selectedEvmAddress),
},
{
queryKey: queryKeys.GET_MUSDSHFVD_BALANCE,
enabled: Boolean(selectedEvmAddress),
},
{ queryKey: queryKeys.GET_EXCHANGE_RATE },
// TEMP: Schema validation is failing in core. Keep commented out until new @metamask/money-account-controller preview release is available.
{ queryKey: queryKeys.GET_VAULT_APY },
{
queryKey: queryKeys.GET_MUSD_EQUIVALENT_VALUE,
enabled: Boolean(selectedEvmAddress),
},
],
}) as [
UseQueryResult<Erc20BalanceResponse>,
UseQueryResult<Erc20BalanceResponse>,
UseQueryResult<ExchangeRateResponse>,
UseQueryResult<VaultApyResponse>,
UseQueryResult<MusdEquivalentValueResponse>,
];

// TODO: Review this logic.
// Compute the per-mUSD fiat rate from Mainnet market data.
// mUSD is a USD-pegged stablecoin so Mainnet market data is reliable even
// when the service is temporarily configured against a different chain.
const musdFiatRate = useMemo(() => {
const musdAddress = MUSD_TOKEN_ADDRESS_BY_CHAIN[CHAIN_IDS.MAINNET];
if (!musdAddress) return undefined;

const checksumAddress = toChecksumAddress(musdAddress);
const chainConfig = networkConfigurations?.[CHAIN_IDS.MAINNET];
const nativeCurrency = chainConfig?.nativeCurrency;
const conversionRate = nativeCurrency
? currencyRates?.[nativeCurrency]?.conversionRate
: undefined;

const priceInNativeCurrency =
tokenMarketData?.[CHAIN_IDS.MAINNET]?.[checksumAddress]?.price ??
tokenMarketData?.[CHAIN_IDS.MAINNET]?.[musdAddress]?.price;

if (!conversionRate || priceInNativeCurrency === undefined)
return undefined;

return new BigNumber(priceInNativeCurrency).times(conversionRate);
}, [tokenMarketData, currencyRates, networkConfigurations]);

const { musdFiat, musdSHFvdFiat, tokenTotal, totalFiat } = useMemo(() => {
// mUSD balance: raw uint256 (6 decimals) → decimal BigNumber
const musdDecimal = musdBalanceResult.data?.balance
? new BigNumber(
fromTokenMinimalUnitString(
musdBalanceResult.data.balance,
MUSD_DECIMALS,
),
)
: new BigNumber(0);

// musdSHFvd balance expressed in mUSD: pre-computed by the service as
// musdSHFvdBalance * exchangeRate / 10^6, returned as a raw uint256 string.
const musdSHFvdDecimal = musdEquivalentBalanceResult.data
?.musdEquivalentValue
? new BigNumber(
fromTokenMinimalUnitString(
musdEquivalentBalanceResult.data.musdEquivalentValue,
MUSD_DECIMALS,
),
)
: new BigNumber(0);

const computedMusdFiat = musdFiatRate
? musdDecimal.times(musdFiatRate)
: undefined;

const computedMusdSHFvdFiat = musdFiatRate
? musdSHFvdDecimal.times(musdFiatRate)
: undefined;

return {
musdFiat: computedMusdFiat,
musdSHFvdFiat: computedMusdSHFvdFiat,
tokenTotal: musdDecimal.plus(musdSHFvdDecimal),
totalFiat:
computedMusdFiat && computedMusdSHFvdFiat
? computedMusdFiat.plus(computedMusdSHFvdFiat)
: undefined,
};
}, [musdBalanceResult.data, musdEquivalentBalanceResult.data, musdFiatRate]);

const musdFiatFormatted = musdFiat ? formatFiat(musdFiat) : undefined;
const musdSHFvdFiatFormatted = musdSHFvdFiat
? formatFiat(musdSHFvdFiat)
: undefined;
const totalFiatFormatted = totalFiat ? formatFiat(totalFiat) : undefined;

return {
musdBalanceResult,
musdShfvdBalanceResult,
exchangeRateResult,
vaultApyResult,
musdEquivalentBalanceResult,
musdFiatFormatted,
musdSHFvdFiatFormatted,
tokenTotal,
totalFiatFormatted,
};
};

export default useMoneyAccountBalance;
4 changes: 3 additions & 1 deletion app/constants/data-services.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
import { MoneyAccountBalanceService } from '@metamask/money-account-controller';

// A list of all names of data services available in the client.
export const DATA_SERVICES: string[] = [];
export const DATA_SERVICES: string[] = [MoneyAccountBalanceService.name];
3 changes: 3 additions & 0 deletions app/core/Engine/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ import { userStorageControllerInit } from './controllers/identity/user-storage-c
import { authenticationControllerInit } from './controllers/identity/authentication-controller-init';
import { earnControllerInit } from './controllers/earn-controller-init';
import { moneyAccountControllerInit } from './controllers/money-account-controller-init';
import { moneyAccountBalanceServiceInit } from './controllers/money-account-balance-service-init';
import { geolocationApiServiceInit } from './controllers/geolocation-api-service-init';
import { geolocationControllerInit } from './controllers/geolocation-controller';
import { rewardsDataServiceInit } from './controllers/rewards-data-service-init';
Expand Down Expand Up @@ -366,6 +367,7 @@ export class Engine {
PredictController: predictControllerInit,
RewardsController: rewardsControllerInit,
RewardsDataService: rewardsDataServiceInit,
MoneyAccountBalanceService: moneyAccountBalanceServiceInit,
DelegationController: DelegationControllerInit,
AddressBookController: addressBookControllerInit,
ConnectivityController: connectivityControllerInit,
Expand Down Expand Up @@ -565,6 +567,7 @@ export class Engine {
BridgeStatusController: controllersByName.BridgeStatusController,
EarnController: earnController,
MoneyAccountController: moneyAccountController,
MoneyAccountBalanceService: controllersByName.MoneyAccountBalanceService,
GeolocationController: geolocationController,
DeFiPositionsController: controllersByName.DeFiPositionsController,
SeedlessOnboardingController: seedlessOnboardingController,
Expand Down
1 change: 1 addition & 0 deletions app/core/Engine/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const STATELESS_NON_CONTROLLER_NAMES = [
'ExecutionService',
'NftDetectionController',
'RewardsDataService',
'MoneyAccountBalanceService',
'StorageService',
'TokenDetectionController',
'WebSocketService',
Expand Down
48 changes: 48 additions & 0 deletions app/core/Engine/controllers/money-account-balance-service-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CHAIN_IDS } from '@metamask/transaction-controller';
import type { MessengerClientInitFunction } from '../types';
import {
MoneyAccountBalanceService,
type MoneyAccountBalanceServiceMessenger,
} from '@metamask/money-account-controller';
import { Hex } from '@metamask/utils';

/**
* TODO: Replace with actual values when the mUSD and Veda Vault are deployed on selected network.
* We'll likely want to retrieve values from build or remote flags.
* */

const TEST_VAULT_ADDRESS = '0xB5F07d769dD60fE54c97dd53101181073DDf21b2' as Hex;
const TEST_ACCOUNTANT_ADDRESS =
'0x800ebc3B74F67EaC27C9CCE4E4FF28b17CdCA173' as Hex;
const ARBITRUM_USDC_ADDRESS =
'0xaf88d065e77c8cC2239327C5EDb3A432268e5831' as Hex;
const ARBITRUM_USDC_DECIMALS = 6;

const TEMP_CONFIG = {
vaultAddress: TEST_VAULT_ADDRESS,
vaultChainId: CHAIN_IDS.ARBITRUM,
accountantAddress: TEST_ACCOUNTANT_ADDRESS,
underlyingTokenAddress: ARBITRUM_USDC_ADDRESS,
underlyingTokenDecimals: ARBITRUM_USDC_DECIMALS,
};

/**
* Initialize the MoneyAccountBalanceService.
*
* @param request - The request object.
* @param request.controllerMessenger - The messenger to use for the service.
* @returns The initialized service.
*/
export const moneyAccountBalanceServiceInit: MessengerClientInitFunction<
MoneyAccountBalanceService,
MoneyAccountBalanceServiceMessenger
> = ({ controllerMessenger }) => {
const controller = new MoneyAccountBalanceService({
messenger: controllerMessenger,
config: {
...TEMP_CONFIG,
},
});

return { controller };
};
5 changes: 5 additions & 0 deletions app/core/Engine/messengers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import {
getEarnControllerMessenger,
} from './earn-controller-messenger';
import { getMoneyAccountControllerMessenger } from './money-account-controller-messenger';
import { getMoneyAccountBalanceServiceMessenger } from './money-account-balance-service-messenger';
import { getGeolocationApiServiceMessenger } from './geolocation-api-service-messenger';
import { getGeolocationControllerMessenger } from './geolocation-controller-messenger';
import { getRewardsDataServiceMessenger } from './rewards-data-service-messenger';
Expand Down Expand Up @@ -408,6 +409,10 @@ export const MESSENGER_FACTORIES = {
getMessenger: getRewardsDataServiceMessenger,
getInitMessenger: noop,
},
MoneyAccountBalanceService: {
getMessenger: getMoneyAccountBalanceServiceMessenger,
getInitMessenger: noop,
},
RampsController: {
getMessenger: getRampsControllerMessenger,
getInitMessenger: getRampsControllerInitMessenger,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
Messenger,
type MessengerActions,
type MessengerEvents,
} from '@metamask/messenger';
import type { MoneyAccountBalanceServiceMessenger } from '@metamask/money-account-controller';
import type { RootMessenger } from '../types';

/**
* Get the messenger for the MoneyAccountBalanceService. This is scoped to the
* actions and events that the service is allowed to handle.
*
* @param rootMessenger - The root messenger.
* @returns The MoneyAccountBalanceServiceMessenger.
*/
export function getMoneyAccountBalanceServiceMessenger(
rootMessenger: RootMessenger,
): MoneyAccountBalanceServiceMessenger {
const messenger = new Messenger<
'MoneyAccountBalanceService',
MessengerActions<MoneyAccountBalanceServiceMessenger>,
MessengerEvents<MoneyAccountBalanceServiceMessenger>,
RootMessenger
>({
namespace: 'MoneyAccountBalanceService',
parent: rootMessenger,
});

rootMessenger.delegate({
messenger,
actions: [
'NetworkController:getNetworkConfigurationByChainId',
'NetworkController:getNetworkClientById',
],
events: [],
});

return messenger;
}
Loading
Loading