Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/assets-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Narrow `AssetsControllerState` types from `Json` to semantic types: `assetsMetadata` → `AssetMetadata`, `assetsBalance` → `AssetBalance`, `assetsPrice` → `AssetPrice`, `assetPreferences` → `AssetPreferences`, `customAssets` → `Caip19AssetId[]` ([#7777](https://github.com/MetaMask/core/pull/7777))

- Replace `viem` dependency with `@ethersproject/abi` for ABI encoding/decoding in `MulticallClient` ([#7839](https://github.com/MetaMask/core/pull/7839))

## [0.1.0]

### Added
Expand All @@ -27,7 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add batch utilities (`divideIntoBatches`, `reduceInBatchesSerially`) for processing arrays in batches ([#7677](https://github.com/MetaMask/core/pull/7677))
- Add `TokenDetector` service for detecting ERC-20 tokens with non-zero balances on a chain ([#7683](https://github.com/MetaMask/core/pull/7683))
- Add `BalanceFetcher` service for fetching token balances for user's imported/detected tokens ([#7684](https://github.com/MetaMask/core/pull/7684))
- Add `viem` dependency for ABI encoding/decoding in MulticallClient
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we remove this changelog entry if this version is already released ?

- Add configurable polling intervals for `RpcDataSource` via `RpcDataSourceConfig` in `initDataSources` ([#7709](https://github.com/MetaMask/core/pull/7709))
- Add comprehensive unit tests for data sources (`AccountsApiDataSource`, `BackendWebsocketDataSource`, `PriceDataSource`, `TokenDataSource`, `SnapDataSource`), `DetectionMiddleware`, and `AssetsController` ([#7714](https://github.com/MetaMask/core/pull/7714))

Expand All @@ -38,7 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Store native token metadata (type, symbol, name, decimals) in `assetsMetadata` derived from `NetworkController` chain status ([#7752](https://github.com/MetaMask/core/pull/7752))
- `AccountsApiDataSource` now includes `assetsMetadata` in response with token info from V5 API ([#7752](https://github.com/MetaMask/core/pull/7752))
- Bump `@metamask/keyring-controller` from `^25.0.0` to `^25.1.0` ([#7713](https://github.com/MetaMask/core/pull/7713))
- Refactor `MulticallClient` to use viem for ABI encoding/decoding instead of manual implementation
- Refactor `RpcDataSource` to delegate polling to `BalanceFetcher` and `TokenDetector` services ([#7709](https://github.com/MetaMask/core/pull/7709))
- Refactor `BalanceFetcher` and `TokenDetector` to extend `StaticIntervalPollingControllerOnly` for independent polling management ([#7709](https://github.com/MetaMask/core/pull/7709))

Expand Down
4 changes: 2 additions & 2 deletions packages/assets-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
},
"dependencies": {
"@ethereumjs/util": "^9.1.0",
"@ethersproject/abi": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@metamask/account-tree-controller": "^4.0.0",
"@metamask/base-controller": "^9.0.0",
Expand All @@ -68,8 +69,7 @@
"@metamask/utils": "^11.9.0",
"async-mutex": "^0.5.0",
"bignumber.js": "^9.1.2",
"lodash": "^4.17.21",
"viem": "^2.44.4"
"lodash": "^4.17.21"
},
"devDependencies": {
"@metamask/auto-changelog": "^3.4.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defaultAbiCoder, Interface } from '@ethersproject/abi';
import type { Hex } from '@metamask/utils';
import { encodeAbiParameters, parseAbiParameters } from 'viem';

import {
decodeAggregate3Response,
Expand Down Expand Up @@ -36,39 +36,37 @@ const MAINNET_CHAIN_ID: ChainId = '0x1' as ChainId;
const UNSUPPORTED_CHAIN_ID: ChainId = '0xfffff' as ChainId;

// =============================================================================
// HELPER FUNCTIONS
// ABI ENCODING HELPERS FOR TESTS
// =============================================================================

/**
* Build a mock aggregate3 response using viem's ABI encoding.
* Each result is (success: bool, returnData: bytes).
* Build a mock aggregate3 response using ethers ABI encoding.
* Encodes (bool success, bytes returnData)[]
*
* @param results - Array of result objects with success flag and optional balance
* @returns The encoded aggregate3 response as hex
*/
function buildMockAggregate3Response(
results: { success: boolean; balance?: string }[],
): `0x${string}` {
// Encode each result's returnData (balance as uint256)
const resultsWithEncodedData = results.map((result) => {
let returnData: `0x${string}` = '0x';
// Build the results array with encoded returnData
const encodedResults = results.map((result) => {
let returnData: string = '0x';
if (result.balance !== undefined) {
// Encode the balance as a uint256
returnData = encodeAbiParameters(parseAbiParameters('uint256'), [
BigInt(result.balance),
]);
// Encode balance as uint256
returnData = defaultAbiCoder.encode(['uint256'], [result.balance]);
}
return {
success: result.success,
returnData,
};
});

// Encode the full aggregate3 response: (bool success, bytes returnData)[]
return encodeAbiParameters(
parseAbiParameters('(bool success, bytes returnData)[]'),
[resultsWithEncodedData],
);
// Encode the full response: (bool success, bytes returnData)[]
return defaultAbiCoder.encode(
['(bool success, bytes returnData)[]'],
[encodedResults],
) as `0x${string}`;
}

// =============================================================================
Expand Down Expand Up @@ -687,6 +685,47 @@ describe('encodeAggregate3', () => {
expect(result).toMatch(/^0x82ad56cb/u);
expect(typeof result).toBe('string');
});

it('should produce valid ABI-encoded calldata that can be decoded', () => {
// Test with multiple calls to verify encoding is correct
const calls = [
{
target: '0x1111111111111111111111111111111111111111' as Address,
allowFailure: true,
callData: '0xabcd' as Hex,
},
{
target: '0x2222222222222222222222222222222222222222' as Address,
allowFailure: false,
callData: '0x1234567890' as Hex,
},
];

const result = encodeAggregate3(calls);

// Verify selector is correct (aggregate3)
expect(result).toMatch(/^0x82ad56cb/u);

// Decode the calldata using the same interface to verify it's valid
const multicall3Abi = new Interface([
'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[])',
]);

const decoded = multicall3Abi.decodeFunctionData('aggregate3', result);
const decodedCalls = decoded[0] as {
target: string;
allowFailure: boolean;
callData: string;
}[];

expect(decodedCalls).toHaveLength(2);
expect(decodedCalls[0].target.toLowerCase()).toBe(calls[0].target);
expect(decodedCalls[0].allowFailure).toBe(true);
expect(decodedCalls[0].callData.toLowerCase()).toBe(calls[0].callData);
expect(decodedCalls[1].target.toLowerCase()).toBe(calls[1].target);
expect(decodedCalls[1].allowFailure).toBe(false);
expect(decodedCalls[1].callData.toLowerCase()).toBe(calls[1].callData);
});
});

describe('decodeAggregate3Response', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Interface } from '@ethersproject/abi';
import type { Hex } from '@metamask/utils';
import type { Abi } from 'viem';
import { decodeFunctionResult, encodeFunctionData } from 'viem';

import type {
Address,
Expand Down Expand Up @@ -55,7 +54,7 @@ const MULTICALL3_ABI = [
inputs: [{ name: 'addr', type: 'address' }],
outputs: [{ name: 'balance', type: 'uint256' }],
},
] as const satisfies Abi;
];

/**
* ERC-20 ABI (subset for balanceOf).
Expand All @@ -68,7 +67,13 @@ const ERC20_ABI = [
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ name: 'balance', type: 'uint256' }],
},
] as const satisfies Abi;
];

/**
* Interface instances for ABI encoding/decoding.
*/
const multicall3Interface = new Interface(MULTICALL3_ABI);
const erc20Interface = new Interface(ERC20_ABI);

// =============================================================================
// CONSTANTS
Expand Down Expand Up @@ -363,7 +368,7 @@ const MULTICALL3_ADDRESS_BY_CHAIN: Record<Hex, Hex> = {
};

// =============================================================================
// ENCODING/DECODING UTILITIES (using viem)
// ENCODING/DECODING UTILITIES (using @ethersproject/abi)
// =============================================================================

/**
Expand All @@ -373,11 +378,9 @@ const MULTICALL3_ADDRESS_BY_CHAIN: Record<Hex, Hex> = {
* @returns The encoded call data.
*/
function encodeBalanceOf(accountAddress: Address): Hex {
return encodeFunctionData({
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [accountAddress],
});
return erc20Interface.encodeFunctionData('balanceOf', [
accountAddress,
]) as Hex;
}

/**
Expand All @@ -387,11 +390,9 @@ function encodeBalanceOf(accountAddress: Address): Hex {
* @returns The encoded call data.
*/
function encodeGetEthBalance(accountAddress: Address): Hex {
return encodeFunctionData({
abi: MULTICALL3_ABI,
functionName: 'getEthBalance',
args: [accountAddress],
});
return multicall3Interface.encodeFunctionData('getEthBalance', [
accountAddress,
]) as Hex;
}

/**
Expand All @@ -403,17 +404,13 @@ function encodeGetEthBalance(accountAddress: Address): Hex {
export function encodeAggregate3(
calls: readonly { target: Address; allowFailure: boolean; callData: Hex }[],
): Hex {
return encodeFunctionData({
abi: MULTICALL3_ABI,
functionName: 'aggregate3',
args: [
calls.map((call) => ({
target: call.target,
allowFailure: call.allowFailure,
callData: call.callData,
})),
],
});
return multicall3Interface.encodeFunctionData('aggregate3', [
calls.map((call) => ({
target: call.target,
allowFailure: call.allowFailure,
callData: call.callData,
})),
]) as Hex;
}

/**
Expand All @@ -428,16 +425,12 @@ export function decodeAggregate3Response(
data: Hex,
callCount: number,
): { success: boolean; returnData: Hex }[] {
const decoded = decodeFunctionResult({
abi: MULTICALL3_ABI,
functionName: 'aggregate3',
data,
});

// decoded is an array of { success: boolean, returnData: `0x${string}` }
const results = decoded as readonly {
const decoded = multicall3Interface.decodeFunctionResult('aggregate3', data);

// decoded[0] is the array of (success, returnData) tuples
const results = decoded[0] as readonly {
success: boolean;
returnData: Hex;
returnData: string;
}[];

if (results.length !== callCount) {
Expand All @@ -446,7 +439,7 @@ export function decodeAggregate3Response(

return results.map((result) => ({
success: result.success,
returnData: result.returnData,
returnData: result.returnData as Hex,
}));
}

Expand All @@ -457,13 +450,12 @@ export function decodeAggregate3Response(
* @returns The decoded balance as a string.
*/
function decodeUint256(data: Hex): string {
const hexData = data.slice(2);
if (hexData.length === 0) {
if (data === '0x' || data.length < 66) {
// Empty or invalid data; treat as 0
return '0';
}
// Take first 64 chars (32 bytes) for uint256
const normalizedHex = hexData.length > 64 ? hexData.slice(0, 64) : hexData;
return BigInt(`0x${normalizedHex}`).toString();
const decoded = erc20Interface.decodeFunctionResult('balanceOf', data);
return decoded[0].toString();
}

// =============================================================================
Expand Down
Loading