Skip to content

Commit

Permalink
feat: Show Staked ETH position in mobile homepage along with other to…
Browse files Browse the repository at this point in the history
…kens (#4879)

## Explanation

**What is the current state of things and why does it need to change?**
- Currently the `metamask-mobile` app using the `assets-controllers` to
get information about account token balances. We need to be able to
support getting a new type of asset on mainnet and holesky chains,
_Staked Ethereum_, which is not a token but represents the amount of ETH
staked using our products.

**What is the solution your changes offer and how does it work?**
- We update the `AssetContractController` with a new method,
`getStakedBalanceForChain`, which gets staked ethereum balances per
account from the Stakewise vault contract. We update the
AccountTrackerController with options to `includeStakedAssets` and to
add a `getStakedBalanceForChain` method.
- We bind `AssetContractController.getStakedBalanceForChain` to
`getStakedBalanceForChain` option property in `metamask-mobile` code and
then set `includeStakingAssets` option to the boolean feature flag for
ETH Staking on Mobile.
- We use the AccountTrackerController state in mobile to update the
account `balance` and now, if enabled the `stakedBalance` as well.

**Are there any changes whose purpose might not obvious to those
unfamiliar with the domain?**
- We don't want to show `stakedBalance` if not on a supported network,
and so return undefined vs defaulting to zero hex. If there is an error
and we are on a supported network, we want to default to zero hex.

**If your primary goal was to update one package but you found you had
to update another one along the way, why did you do so?**
- There is 1 package affected

**If you had to upgrade a dependency, why did you do so?**
- No need to update dependency

## References

**Are there any issues that this pull request is tied to?**
- Relates to https://consensyssoftware.atlassian.net/browse/STAKE-817

**Are there other links that reviewers should consult to understand
these changes better?**
- MetaMask/metamask-mobile#12146 PR to MM mobile
with patched `[email protected]`

**Are there client or consumer pull requests to adopt any breaking
changes?**
- No

## Changelog

### `@metamask/assets-controllers`

**ADDED**: AssetsContractController.getStakedBalanceForChain method to
get staked ethereum balance for an address
**ADDED**: AccountTrackerController options `includeStakedEthereum` and
`getStakedBalanceForChain` for turning on staked balance functionality
and providing a method to do so

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've highlighted breaking changes using the "BREAKING" category
above as appropriate
- [x] I've prepared draft pull requests for clients and consumer
packages to resolve any breaking changes
  • Loading branch information
nickewansmith authored Nov 4, 2024
1 parent 951e026 commit f5ad522
Show file tree
Hide file tree
Showing 6 changed files with 686 additions and 11 deletions.
324 changes: 324 additions & 0 deletions packages/assets-controllers/src/AccountTrackerController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,123 @@ describe('AccountTrackerController', () => {
},
);
});

it('should update staked balance when includeStakedAssets is enabled', async () => {
mockedQuery
.mockReturnValueOnce(Promise.resolve('0x10'))
.mockReturnValueOnce(Promise.resolve('0x11'));

await withController(
{
options: {
includeStakedAssets: true,
getStakedBalanceForChain: jest.fn().mockResolvedValue('0x1'),
},
isMultiAccountBalancesEnabled: false,
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1, ACCOUNT_2],
},
async ({ controller }) => {
await controller.refresh();

expect(controller.state).toStrictEqual({
accounts: {
[CHECKSUM_ADDRESS_1]: { balance: '0x10', stakedBalance: '0x1' },
[CHECKSUM_ADDRESS_2]: { balance: '0x0' },
},
accountsByChainId: {
'0x1': {
[CHECKSUM_ADDRESS_1]: {
balance: '0x10',
stakedBalance: '0x1',
},
[CHECKSUM_ADDRESS_2]: {
balance: '0x0',
},
},
},
});
},
);
});

it('should not update staked balance when includeStakedAssets is disabled', async () => {
mockedQuery
.mockReturnValueOnce(Promise.resolve('0x13'))
.mockReturnValueOnce(Promise.resolve('0x14'));

await withController(
{
options: {
includeStakedAssets: false,
getStakedBalanceForChain: jest.fn().mockResolvedValue('0x1'),
},
isMultiAccountBalancesEnabled: false,
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1, ACCOUNT_2],
},
async ({ controller }) => {
await controller.refresh();

expect(controller.state).toStrictEqual({
accounts: {
[CHECKSUM_ADDRESS_1]: { balance: '0x13' },
[CHECKSUM_ADDRESS_2]: { balance: '0x0' },
},
accountsByChainId: {
'0x1': {
[CHECKSUM_ADDRESS_1]: {
balance: '0x13',
},
[CHECKSUM_ADDRESS_2]: {
balance: '0x0',
},
},
},
});
},
);
});

it('should update staked balance when includeStakedAssets and multi-account is enabled', async () => {
mockedQuery
.mockReturnValueOnce(Promise.resolve('0x11'))
.mockReturnValueOnce(Promise.resolve('0x12'));

await withController(
{
options: {
includeStakedAssets: true,
getStakedBalanceForChain: jest.fn().mockResolvedValue('0x1'),
},
isMultiAccountBalancesEnabled: true,
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1, ACCOUNT_2],
},
async ({ controller }) => {
await controller.refresh();

expect(controller.state).toStrictEqual({
accounts: {
[CHECKSUM_ADDRESS_1]: { balance: '0x11', stakedBalance: '0x1' },
[CHECKSUM_ADDRESS_2]: { balance: '0x12', stakedBalance: '0x1' },
},
accountsByChainId: {
'0x1': {
[CHECKSUM_ADDRESS_1]: {
balance: '0x11',
stakedBalance: '0x1',
},
[CHECKSUM_ADDRESS_2]: {
balance: '0x12',
stakedBalance: '0x1',
},
},
},
});
},
);
});
});

describe('with networkClientId', () => {
Expand Down Expand Up @@ -438,6 +555,185 @@ describe('AccountTrackerController', () => {
},
);
});

it('should update staked balance when includeStakedAssets is enabled', async () => {
const networkClientId = 'holesky';
mockedQuery
.mockReturnValueOnce(Promise.resolve('0x10'))
.mockReturnValueOnce(Promise.resolve('0x11'));

await withController(
{
options: {
includeStakedAssets: true,
getStakedBalanceForChain: jest.fn().mockResolvedValue('0x1'),
},
isMultiAccountBalancesEnabled: false,
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1, ACCOUNT_2],
networkClientById: {
[networkClientId]: buildCustomNetworkClientConfiguration({
chainId: '0x4268',
}),
},
},
async ({ controller }) => {
await controller.refresh();

expect(controller.state).toStrictEqual({
accounts: {
[CHECKSUM_ADDRESS_1]: { balance: '0x10', stakedBalance: '0x1' },
[CHECKSUM_ADDRESS_2]: { balance: '0x0' },
},
accountsByChainId: {
'0x1': {
[CHECKSUM_ADDRESS_1]: {
balance: '0x10',
stakedBalance: '0x1',
},
[CHECKSUM_ADDRESS_2]: {
balance: '0x0',
},
},
},
});
},
);
});

it('should not update staked balance when includeStakedAssets is disabled', async () => {
const networkClientId = 'holesky';
mockedQuery
.mockReturnValueOnce(Promise.resolve('0x13'))
.mockReturnValueOnce(Promise.resolve('0x14'));

await withController(
{
options: {
includeStakedAssets: false,
getStakedBalanceForChain: jest.fn().mockResolvedValue('0x1'),
},
isMultiAccountBalancesEnabled: false,
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1, ACCOUNT_2],
networkClientById: {
[networkClientId]: buildCustomNetworkClientConfiguration({
chainId: '0x4268',
}),
},
},
async ({ controller }) => {
await controller.refresh();

expect(controller.state).toStrictEqual({
accounts: {
[CHECKSUM_ADDRESS_1]: { balance: '0x13' },
[CHECKSUM_ADDRESS_2]: { balance: '0x0' },
},
accountsByChainId: {
'0x1': {
[CHECKSUM_ADDRESS_1]: {
balance: '0x13',
},
[CHECKSUM_ADDRESS_2]: {
balance: '0x0',
},
},
},
});
},
);
});

it('should update staked balance when includeStakedAssets and multi-account is enabled', async () => {
const networkClientId = 'holesky';
mockedQuery
.mockReturnValueOnce(Promise.resolve('0x11'))
.mockReturnValueOnce(Promise.resolve('0x12'));

await withController(
{
options: {
includeStakedAssets: true,
getStakedBalanceForChain: jest.fn().mockResolvedValue('0x1'),
},
isMultiAccountBalancesEnabled: true,
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1, ACCOUNT_2],
networkClientById: {
[networkClientId]: buildCustomNetworkClientConfiguration({
chainId: '0x4268',
}),
},
},
async ({ controller }) => {
await controller.refresh();

expect(controller.state).toStrictEqual({
accounts: {
[CHECKSUM_ADDRESS_1]: { balance: '0x11', stakedBalance: '0x1' },
[CHECKSUM_ADDRESS_2]: { balance: '0x12', stakedBalance: '0x1' },
},
accountsByChainId: {
'0x1': {
[CHECKSUM_ADDRESS_1]: {
balance: '0x11',
stakedBalance: '0x1',
},
[CHECKSUM_ADDRESS_2]: {
balance: '0x12',
stakedBalance: '0x1',
},
},
},
});
},
);
});

it('should not update staked balance when includeStakedAssets and multi-account is enabled if network unsupported', async () => {
const networkClientId = 'polygon';
mockedQuery
.mockReturnValueOnce(Promise.resolve('0x11'))
.mockReturnValueOnce(Promise.resolve('0x12'));

await withController(
{
options: {
includeStakedAssets: true,
getStakedBalanceForChain: jest.fn().mockResolvedValue(undefined),
},
isMultiAccountBalancesEnabled: true,
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1, ACCOUNT_2],
networkClientById: {
[networkClientId]: buildCustomNetworkClientConfiguration({
chainId: '0x89',
}),
},
},
async ({ controller }) => {
await controller.refresh();

expect(controller.state).toStrictEqual({
accounts: {
[CHECKSUM_ADDRESS_1]: { balance: '0x11' },
[CHECKSUM_ADDRESS_2]: { balance: '0x12' },
},
accountsByChainId: {
'0x1': {
[CHECKSUM_ADDRESS_1]: {
balance: '0x11',
},
[CHECKSUM_ADDRESS_2]: {
balance: '0x12',
},
},
},
});
},
);
});
});
});

Expand All @@ -462,6 +758,33 @@ describe('AccountTrackerController', () => {
},
);
});

it('should sync staked balance with addresses', async () => {
await withController(
{
options: {
includeStakedAssets: true,
getStakedBalanceForChain: jest.fn().mockResolvedValue('0x1'),
},
isMultiAccountBalancesEnabled: true,
selectedAccount: ACCOUNT_1,
listAccounts: [],
},
async ({ controller }) => {
mockedQuery
.mockReturnValueOnce(Promise.resolve('0x10'))
.mockReturnValueOnce(Promise.resolve('0x20'));
const result = await controller.syncBalanceWithAddresses([
ADDRESS_1,
ADDRESS_2,
]);
expect(result[ADDRESS_1].balance).toBe('0x10');
expect(result[ADDRESS_2].balance).toBe('0x20');
expect(result[ADDRESS_1].stakedBalance).toBe('0x1');
expect(result[ADDRESS_2].stakedBalance).toBe('0x1');
},
);
});
});

it('should call refresh every interval on legacy polling', async () => {
Expand Down Expand Up @@ -647,6 +970,7 @@ async function withController<ReturnValue>(

const controller = new AccountTrackerController({
messenger: accountTrackerMessenger,
getStakedBalanceForChain: jest.fn(),
...options,
});

Expand Down
Loading

0 comments on commit f5ad522

Please sign in to comment.