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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/core-monorepo",
"version": "937.0.0",
"version": "938.0.0",
"private": true,
"description": "Monorepo for packages shared between MetaMask clients",
"repository": {
Expand Down
4 changes: 4 additions & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Malicious tokens that slip through are caught by the periodic rescan (runs daily by default)
- Bump `@metamask/transaction-controller` from `^64.3.0` to `^64.4.0` ([#8585](https://github.com/MetaMask/core/pull/8585))

### Fixed

- Fix `selectAssetsBySelectedAccountGroup` crashing when an account referenced in the account tree is missing from internal accounts ([#8604](https://github.com/MetaMask/core/pull/8604))

## [104.3.0]

### Added
Expand Down
12 changes: 12 additions & 0 deletions packages/assets-controllers/src/selectors/token-selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,18 @@ describe('token-selectors', () => {
expect(result).toStrictEqual(expectedMockResult);
});

it('skips accounts referenced in accountTree but missing from internalAccounts', () => {
const state = cloneDeep(mockedMergedState);

state.accountTree.wallets['entropy:01K1TJY9QPSCKNBSVGZNG510GJ'].groups[
'entropy:01K1TJY9QPSCKNBSVGZNG510GJ/0'
].accounts.push('non-existent-account-id');

const result = selectAssetsBySelectedAccountGroup(state);

expect(result).toStrictEqual(expectedMockResult);
});

it('returns no tokens if there is no selected account group', () => {
const result = selectAssetsBySelectedAccountGroup({
...mockedMergedState,
Expand Down
4 changes: 4 additions & 0 deletions packages/assets-controllers/src/selectors/token-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ const selectAccountsToGroupIdMap = createAssetListSelector(
for (const accountId of accounts) {
const internalAccount = internalAccounts.accounts[accountId];

if (!internalAccount) {
continue;
}

accountsMap[
// TODO: We would not need internalAccounts if evmTokens state had the accountId
internalAccount.type.startsWith('eip155')
Expand Down
4 changes: 4 additions & 0 deletions packages/eth-json-rpc-middleware/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Allow Advanced Permissions `metadata` in signTypedData V4 requests ([#8603](https://github.com/MetaMask/core/pull/8603))

## [23.1.2]

### Changed
Expand Down
117 changes: 117 additions & 0 deletions packages/eth-json-rpc-middleware/src/utils/validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,122 @@ describe('Validation Utils', () => {

expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.');
});

describe('metadata', () => {
const baseTypedData = {
types: {
EIP712Domain: [{ name: 'name', type: 'string' }],
},
primaryType: 'EIP712Domain',
domain: {},
message: {},
};

it('does not throw when metadata has exactly justification and origin as strings', () => {
const data = JSON.stringify({
...baseTypedData,
metadata: {
justification: 'Permission to spend tokens',
origin: 'https://example.com',
},
});

expect(() => validateTypedMessageKeys(data)).not.toThrow();
});

it('does not throw when metadata is the only top-level key', () => {
const data = JSON.stringify({
metadata: {
justification: 'Permission to spend tokens',
origin: 'https://example.com',
},
});

expect(() => validateTypedMessageKeys(data)).not.toThrow();
});

it.each([
['null', null],
['a string', 'not-an-object'],
['a number', 42],
['a boolean', true],
['an array', ['justification', 'origin']],
])('throws when metadata is %s', (_label, value) => {
const data = JSON.stringify({
...baseTypedData,
metadata: value,
});

expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.');
});

it('throws when metadata is missing justification', () => {
const data = JSON.stringify({
...baseTypedData,
metadata: {
origin: 'https://example.com',
},
});

expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.');
});

it('throws when metadata is missing origin', () => {
const data = JSON.stringify({
...baseTypedData,
metadata: {
justification: 'Permission to spend tokens',
},
});

expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.');
});

it('throws when metadata.justification is not a string', () => {
const data = JSON.stringify({
...baseTypedData,
metadata: {
justification: 123,
origin: 'https://example.com',
},
});

expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.');
});

it('throws when metadata.origin is not a string', () => {
const data = JSON.stringify({
...baseTypedData,
metadata: {
justification: 'Permission to spend tokens',
origin: 123,
},
});

expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.');
});

it('throws when metadata has an extraneous third key', () => {
const data = JSON.stringify({
...baseTypedData,
metadata: {
justification: 'Permission to spend tokens',
origin: 'https://example.com',
extra: 'unexpected',
},
});

expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.');
});

it('throws when metadata is an empty object', () => {
const data = JSON.stringify({
...baseTypedData,
metadata: {},
});

expect(() => validateTypedMessageKeys(data)).toThrow('Invalid input.');
});
});
});
});
28 changes: 27 additions & 1 deletion packages/eth-json-rpc-middleware/src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,38 @@ export function validateTypedDataForPrototypePollution(data: string): void {
*/
export function validateTypedMessageKeys(data: string): void {
const parsedData = parseTypedMessage(data);
const allowedKeys = new Set(Object.keys(TYPED_MESSAGE_SCHEMA.properties));
const allowedKeys = new Set([
...Object.keys(TYPED_MESSAGE_SCHEMA.properties),
'metadata',
]);
const hasExtraneousKey = Object.keys(parsedData).some(
(key) => !allowedKeys.has(key),
);

if (hasExtraneousKey) {
throw rpcErrors.invalidInput();
}

// Advanced Permissions adds `metadata: { justification: string, origin: string }` to eth_signTypedData requests.
// see GatorPermissionsController.decodePermissionFromPermissionContextForOrigin for more details.
const { metadata } = parsedData as { metadata?: unknown };
if (metadata !== undefined) {
if (typeof metadata !== 'object' || metadata === null) {
throw rpcErrors.invalidInput();
}

const { justification, origin } = metadata as {
justification?: unknown;
origin?: unknown;
};

if (typeof justification !== 'string' || typeof origin !== 'string') {
throw rpcErrors.invalidInput();
}

// we only need to check the keys length, because we already checked the known keys (justification and origin).
if (Object.keys(metadata).length !== 2) {
throw rpcErrors.invalidInput();
}
}
}
5 changes: 4 additions & 1 deletion packages/passkey-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.0.0]

### Added

- Initial `@metamask/passkey-controller` ([#8422](https://github.com/MetaMask/core/pull/8422)): `PasskeyController` for WebAuthn passkey vault key protection (HKDF-derived keys, AES-256-GCM wrap/unwrap), PRF or `userHandle` derivation, challenge-keyed `CeremonyManager`, enrollment/unlock/renewal flows, `verifyPasskeyAuthentication`, selectors, and exported ceremony timing constants.
Expand All @@ -21,4 +23,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Registration verification requires the credential `id`/`rawId` to match the credential id in authenticator data; vault wrapping key derivation uses that verified credential id so enrollment keys align with the stored credential.
- Registration options request attestation conveyance `'none'` so clients are not asked for direct attestation formats the verifier does not implement (`none` and self-attested `packed` only).

[Unreleased]: https://github.com/MetaMask/core/
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/passkey-controller@1.0.0...HEAD
[1.0.0]: https://github.com/MetaMask/core/releases/tag/@metamask/passkey-controller@1.0.0
2 changes: 1 addition & 1 deletion packages/passkey-controller/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/passkey-controller",
"version": "0.0.0",
"version": "1.0.0",
"description": "Controller and utilities for passkey-based wallet unlock",
"keywords": [
"Ethereum",
Expand Down
4 changes: 4 additions & 0 deletions packages/social-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `SocialService.fetchPositionById` method exposing `GET /v1/traders/position/:positionId`, returning a single `Position` by ID ([#8602](https://github.com/MetaMask/core/pull/8602))

## [2.1.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ export type SocialServiceFetchFollowersAction = {
handler: SocialService['fetchFollowers'];
};

/**
* Fetches a single position by its unique ID.
*
* Calls `GET ${baseUrl}/traders/position/${positionId}`.
*
* @param options - Options bag.
* @param options.positionId - Unique position ID (UUID).
* @returns The position.
*/
export type SocialServiceFetchPositionByIdAction = {
type: `SocialService:fetchPositionById`;
handler: SocialService['fetchPositionById'];
};

/**
* Fetches the list of traders the current user is following.
*
Expand Down Expand Up @@ -136,6 +150,7 @@ export type SocialServiceMethodActions =
| SocialServiceFetchOpenPositionsAction
| SocialServiceFetchClosedPositionsAction
| SocialServiceFetchFollowersAction
| SocialServiceFetchPositionByIdAction
| SocialServiceFetchFollowingAction
| SocialServiceFollowAction
| SocialServiceUnfollowAction;
79 changes: 79 additions & 0 deletions packages/social-controllers/src/SocialService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,85 @@ describe('SocialService', () => {
});
});

describe('fetchPositionById', () => {
it('fetches position from correct endpoint', async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve(mockPosition),
});

const service = createService();
const result = await service.fetchPositionById({
positionId: 'position-1',
});

expect(result).toStrictEqual(mockPosition);
expect(mockFetch).toHaveBeenCalledWith(
`${V1_URL}/traders/position/position-1`,
{ headers: { Authorization: `Bearer ${MOCK_TOKEN}` } },
);
});

it('encodes the positionId in the URL', async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve(mockPosition),
});

const service = createService();
await service.fetchPositionById({ positionId: 'pos/with/slashes' });

expect(mockFetch).toHaveBeenCalledWith(
`${V1_URL}/traders/position/pos%2Fwith%2Fslashes`,
{ headers: { Authorization: `Bearer ${MOCK_TOKEN}` } },
);
});

it('throws HttpError on non-ok response', async () => {
mockFetch.mockResolvedValue({ ok: false, status: 404 });

const service = createService();

await expect(
service.fetchPositionById({ positionId: 'position-1' }),
).rejects.toThrow(
`${SocialServiceErrorMessage.FETCH_POSITION_BY_ID_FAILED}: 404`,
);
});

it('throws when response schema is invalid', async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve({ positionId: 123 }),
});

const service = createService();

await expect(
service.fetchPositionById({ positionId: 'position-1' }),
).rejects.toThrow(
SocialServiceErrorMessage.FETCH_POSITION_BY_ID_INVALID_RESPONSE,
);
});

it('returns cached result on repeated calls with same positionId', async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: () => Promise.resolve(mockPosition),
});

const service = createService();
await service.fetchPositionById({ positionId: 'position-1' });
await service.fetchPositionById({ positionId: 'position-1' });

expect(mockFetch).toHaveBeenCalledTimes(1);
});
});

describe('fetchFollowing', () => {
const mockFollowingResponse = {
following: [mockProfileSummary],
Expand Down
Loading
Loading