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
3 changes: 0 additions & 3 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,6 @@
"@typescript-eslint/no-unused-vars": {
"count": 1
},
"@typescript-eslint/prefer-nullish-coalescing": {
"count": 4
},
"@typescript-eslint/prefer-optional-chain": {
"count": 4
},
Expand Down
1 change: 1 addition & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Bump `@metamask/accounts-controller` from `^35.0.0` to `^35.0.1` ([#7604](https://github.com/MetaMask/core/pull/7604))
- Bump `@metamask/polling-controller` from `^16.0.0` to `^16.0.1` ([#7604](https://github.com/MetaMask/core/pull/7604))
- Update Plasma (0x2611) mapping to eip155:9745/erc20:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee for XPL ([#7601](https://github.com/MetaMask/core/pull/7601))
- `TokensController.watchAsset` now supports optional origin/page metadata and safely falls back for empty origins to avoid rejected approvals ([#7612](https://github.com/MetaMask/core/pull/7612))

## [95.1.0]

Expand Down
67 changes: 67 additions & 0 deletions packages/assets-controllers/src/TokensController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2393,6 +2393,73 @@ describe('TokensController', () => {
});
});

it('falls back to ORIGIN_METAMASK when origin is empty string', async () => {
await withController(async ({ controller, approvalController }) => {
const requestId = '12345';
const addAndShowApprovalRequestSpy = jest
.spyOn(approvalController, 'addAndShowApprovalRequest')
.mockResolvedValue(undefined);
const asset = buildToken();
ContractMock.mockReturnValue(
buildMockEthersERC721Contract({ supportsInterface: false }),
);
uuidV1Mock.mockReturnValue(requestId);

await controller.watchAsset({
asset,
type: 'ERC20',
origin: '',
networkClientId: 'mainnet',
});

expect(addAndShowApprovalRequestSpy).toHaveBeenCalledWith({
id: requestId,
origin: ORIGIN_METAMASK,
type: ApprovalType.WatchAsset,
requestData: {
id: requestId,
interactingAddress: '0x1',
asset,
},
});
});
});

it('uses origin param when requestMetadata.origin is empty string', async () => {
await withController(async ({ controller, approvalController }) => {
const requestId = '12345';
const addAndShowApprovalRequestSpy = jest
.spyOn(approvalController, 'addAndShowApprovalRequest')
.mockResolvedValue(undefined);
const asset = buildToken();
ContractMock.mockReturnValue(
buildMockEthersERC721Contract({ supportsInterface: false }),
);
uuidV1Mock.mockReturnValue(requestId);

await controller.watchAsset({
asset,
type: 'ERC20',
origin: 'https://example.test',
requestMetadata: {
origin: '',
},
networkClientId: 'mainnet',
});

expect(addAndShowApprovalRequestSpy).toHaveBeenCalledWith({
id: requestId,
origin: 'https://example.test',
type: ApprovalType.WatchAsset,
requestData: {
id: requestId,
interactingAddress: '0x1',
asset,
},
});
});
});

it('stores token correctly under interacting address if user confirms', async () => {
const chainId = ChainId.sepolia;

Expand Down
96 changes: 67 additions & 29 deletions packages/assets-controllers/src/TokensController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import type {
} from '@metamask/network-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import { isStrictHexString } from '@metamask/utils';
import type { Hex } from '@metamask/utils';
import type { Hex, Json } from '@metamask/utils';
import { Mutex } from 'async-mutex';
import type { Patch } from 'immer';
import { cloneDeep } from 'lodash';
Expand Down Expand Up @@ -75,6 +75,21 @@ type SuggestedAssetMeta = {
type: string;
asset: Token;
interactingAddress: string;
origin?: string;
pageMeta?: Record<string, Json>;
};

type WatchAssetRequestMetadata = {
origin?: string;
pageMeta?: Record<string, Json>;
};

const getNonEmptyString = (
...candidates: (string | undefined)[]
): string | undefined => {
return candidates.find(
(candidate) => typeof candidate === 'string' && candidate.trim() !== '',
);
};

/**
Expand Down Expand Up @@ -262,7 +277,7 @@ export class TokensController extends BaseController<
const updatedAllTokens = cloneDeep(allTokens);

for (const [chainId, chainCache] of Object.entries(tokensChainsCache)) {
const chainData = chainCache?.data || {};
const chainData = chainCache?.data ?? {};

if (updatedAllTokens[chainId as Hex]) {
if (updatedAllTokens[chainId as Hex][selectedAddress]) {
Expand Down Expand Up @@ -433,11 +448,11 @@ export class TokensController extends BaseController<

try {
address = toChecksumHexAddress(address);
const tokens = allTokens[chainIdToUse]?.[accountAddress] || [];
const tokens = allTokens[chainIdToUse]?.[accountAddress] ?? [];
const ignoredTokens =
allIgnoredTokens[chainIdToUse]?.[accountAddress] || [];
allIgnoredTokens[chainIdToUse]?.[accountAddress] ?? [];
const detectedTokens =
allDetectedTokens[chainIdToUse]?.[accountAddress] || [];
allDetectedTokens[chainIdToUse]?.[accountAddress] ?? [];
const newTokens: Token[] = [...tokens];
const [isERC721, tokenMetadata] = await Promise.all([
this.#detectIsERC721(address, networkClientId),
Expand All @@ -449,13 +464,14 @@ export class TokensController extends BaseController<
symbol,
decimals,
image:
image ||
formatIconUrlWithProxy({
chainId: chainIdToUse,
tokenAddress: address,
}),
image && image.trim() !== ''
? image
: formatIconUrlWithProxy({
chainId: chainIdToUse,
tokenAddress: address,
}),
isERC721,
aggregators: formatAggregatorNames(tokenMetadata?.aggregators || []),
aggregators: formatAggregatorNames(tokenMetadata?.aggregators ?? []),
name,
...(tokenMetadata?.rwaData && { rwaData: tokenMetadata.rwaData }),
};
Expand Down Expand Up @@ -517,7 +533,7 @@ export class TokensController extends BaseController<

// Used later to dedupe imported tokens
const newTokensMap = [
...(allTokens[interactingChainId]?.[this.#getSelectedAccount().address] ||
...(allTokens[interactingChainId]?.[this.#getSelectedAccount().address] ??
[]),
...tokensToImport,
].reduce<{ [address: string]: Token }>((output, token) => {
Expand Down Expand Up @@ -594,14 +610,14 @@ export class TokensController extends BaseController<
const { allTokens, allDetectedTokens, allIgnoredTokens } = this.state;
const ignoredTokensMap: { [key: string]: true } = {};
const ignoredTokens =
allIgnoredTokens[interactingChainId]?.[this.#getSelectedAddress()] || [];
allIgnoredTokens[interactingChainId]?.[this.#getSelectedAddress()] ?? [];
let newIgnoredTokens: string[] = [...ignoredTokens];

const tokens =
allTokens[interactingChainId]?.[this.#getSelectedAddress()] || [];
allTokens[interactingChainId]?.[this.#getSelectedAddress()] ?? [];

const detectedTokens =
allDetectedTokens[interactingChainId]?.[this.#getSelectedAddress()] || [];
allDetectedTokens[interactingChainId]?.[this.#getSelectedAddress()] ?? [];

const checksummedTokenAddresses = tokenAddressesToIgnore.map((address) => {
const checksumAddress = toChecksumHexAddress(address);
Expand Down Expand Up @@ -721,9 +737,9 @@ export class TokensController extends BaseController<
// Re-point `tokens` and `detectedTokens` to keep them referencing the current chain/account.
const selectedAddress = this.#getSelectedAddress();

newTokens = newAllTokens?.[chainId]?.[selectedAddress] || [];
newTokens = newAllTokens?.[chainId]?.[selectedAddress] ?? [];
newDetectedTokens =
newAllDetectedTokens?.[chainId]?.[selectedAddress] || [];
newAllDetectedTokens?.[chainId]?.[selectedAddress] ?? [];

this.update((state) => {
state.allTokens = newAllTokens;
Expand Down Expand Up @@ -836,18 +852,27 @@ export class TokensController extends BaseController<
* @param options.type - The asset type.
* @param options.interactingAddress - The address of the account that is requesting to watch the asset.
* @param options.networkClientId - Network Client ID.
* @param options.origin - The origin to set on the approval request.
* @param options.pageMeta - The metadata for the page initiating the request.
* @param options.requestMetadata - Metadata for the request, including pageMeta and origin.
* @returns A promise that resolves if the asset was watched successfully, and rejects otherwise.
*/
async watchAsset({
asset,
type,
interactingAddress,
networkClientId,
origin,
pageMeta,
requestMetadata,
}: {
asset: Token;
type: string;
interactingAddress?: string;
networkClientId: NetworkClientId;
origin?: string;
pageMeta?: Record<string, Json>;
requestMetadata?: WatchAssetRequestMetadata;
}): Promise<void> {
if (type !== ERC20) {
throw new Error(`Asset of type ${type} not supported`);
Expand Down Expand Up @@ -955,6 +980,8 @@ export class TokensController extends BaseController<
time: Date.now(),
type,
interactingAddress: selectedAddress,
origin: getNonEmptyString(requestMetadata?.origin, origin),
pageMeta: requestMetadata?.pageMeta ?? pageMeta,
};

await this.#requestApproval(suggestedAssetMeta);
Expand Down Expand Up @@ -1079,22 +1106,33 @@ export class TokensController extends BaseController<
}

async #requestApproval(suggestedAssetMeta: SuggestedAssetMeta) {
const requestData: Record<string, Json> = {
id: suggestedAssetMeta.id,
interactingAddress: suggestedAssetMeta.interactingAddress,
asset: {
address: suggestedAssetMeta.asset.address,
decimals: suggestedAssetMeta.asset.decimals,
symbol: suggestedAssetMeta.asset.symbol,
image:
suggestedAssetMeta.asset.image &&
suggestedAssetMeta.asset.image.trim() !== ''
? suggestedAssetMeta.asset.image
: null,
},
};
if (suggestedAssetMeta.pageMeta) {
requestData.metadata = {
pageMeta: suggestedAssetMeta.pageMeta,
};
}

return this.messenger.call(
'ApprovalController:addRequest',
{
id: suggestedAssetMeta.id,
origin: ORIGIN_METAMASK,
origin: getNonEmptyString(suggestedAssetMeta.origin) ?? ORIGIN_METAMASK,
type: ApprovalType.WatchAsset,
requestData: {
id: suggestedAssetMeta.id,
interactingAddress: suggestedAssetMeta.interactingAddress,
asset: {
address: suggestedAssetMeta.asset.address,
decimals: suggestedAssetMeta.asset.decimals,
symbol: suggestedAssetMeta.asset.symbol,
image: suggestedAssetMeta.asset.image || null,
},
},
requestData,
},
true,
);
Expand All @@ -1110,7 +1148,7 @@ export class TokensController extends BaseController<
'AccountsController:getAccount',
this.#selectedAccountId,
);
return account?.address || '';
return account?.address ?? '';
}

/**
Expand Down
Loading