Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
75f7f40
Attempt to decode a permission if a request is received to sign a del…
jeffsmale90 Sep 16, 2025
2151dc7
Add test coverage for new functionality
jeffsmale90 Sep 17, 2025
9fcb4c4
Add line to CHANGELOG
jeffsmale90 Sep 17, 2025
47460e5
Gracefully handle case where delegate, delegator, or caveats is not s…
jeffsmale90 Sep 17, 2025
b5b431f
Fix unrelated lint warnings
jeffsmale90 Sep 17, 2025
0973497
Refactor and simplification as per PR feedback
jeffsmale90 Sep 17, 2025
48d4d7b
Move execution-permissions to delegations to be more general purpose
jeffsmale90 Sep 17, 2025
e7bc32d
Add line to GatorPermissionsController changelog
jeffsmale90 Sep 18, 2025
510fa3c
Merge branch 'main' into feat/decodePermission_signatureController
jeffsmale90 Sep 18, 2025
0efb820
Fix GatorPermissionsController tests to expect synchronous decodePerm…
jeffsmale90 Sep 18, 2025
2eee797
Merge branch 'main' into feat/decodePermission_signatureController
jeffsmale90 Sep 18, 2025
eccaf79
Import type from @metamask/gator-permissions-controller, rather than …
jeffsmale90 Sep 18, 2025
24529ad
Merge branch 'main' into feat/decodePermission_signatureController
jeffsmale90 Sep 18, 2025
f3a52cb
Merge branch 'main' into feat/decodePermission_signatureController
jeffsmale90 Sep 19, 2025
cd803cd
Update SignatureController dependency on GatorPermissionsController
jeffsmale90 Sep 19, 2025
aabc055
Merge branch 'main' into feat/decodePermission_signatureController
jeffsmale90 Sep 22, 2025
a3a38b6
Ensure chainId is number when decoding permissions from signTypedData
jeffsmale90 Sep 22, 2025
66b3488
Merge branch 'main' into feat/decodePermission_signatureController
jeffsmale90 Sep 22, 2025
51b1155
Merge branch 'main' into feat/decodePermission_signatureController
jeffsmale90 Sep 22, 2025
b7b105d
Merge branch 'main' into feat/decodePermission_signatureController
jeffsmale90 Sep 23, 2025
7b53161
Merge branch 'main' into feat/decodePermission_signatureController
jeffsmale90 Sep 23, 2025
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
15 changes: 2 additions & 13 deletions eslint-warning-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -349,21 +349,10 @@
"jest/no-conditional-in-test": 1
},
"packages/signature-controller/src/SignatureController.ts": {
"@typescript-eslint/no-unsafe-enum-comparison": 4
},
"packages/signature-controller/src/utils/decoding-api.test.ts": {
"import-x/order": 1,
"jsdoc/tag-lines": 1
},
"packages/signature-controller/src/utils/decoding-api.ts": {
"import-x/order": 1
},
"packages/signature-controller/src/utils/normalize.test.ts": {
"import-x/order": 1
"@typescript-eslint/no-unsafe-enum-comparison": 3
},
"packages/signature-controller/src/utils/normalize.ts": {
"@typescript-eslint/no-unused-vars": 1,
"jsdoc/tag-lines": 2
"@typescript-eslint/no-unused-vars": 1
},
"packages/signature-controller/src/utils/validation.ts": {
"@typescript-eslint/no-base-to-string": 1,
Expand Down
1 change: 1 addition & 0 deletions packages/signature-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add two new controller state metadata properties: `includeInStateLogs` and `usedInUi` ([#6473](https://github.com/MetaMask/core/pull/6473))
- Decode delegation permissions using `@metamask/gator-permissions-controller` when calling `newUnsignedTypedMessage` ([#6619](https://github.com/MetaMask/core/pull/6619))

### Changed

Expand Down
2 changes: 2 additions & 0 deletions packages/signature-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@metamask/accounts-controller": "^33.1.0",
"@metamask/approval-controller": "^7.1.3",
"@metamask/auto-changelog": "^3.4.4",
"@metamask/gator-permissions-controller": "^0.2.0",
"@metamask/keyring-controller": "^23.1.0",
"@metamask/logging-controller": "^6.0.4",
"@metamask/network-controller": "^24.2.0",
Expand All @@ -73,6 +74,7 @@
"peerDependencies": {
"@metamask/accounts-controller": "^33.0.0",
"@metamask/approval-controller": "^7.0.0",
"@metamask/gator-permissions-controller": "^0.2.0",
"@metamask/keyring-controller": "^23.0.0",
"@metamask/logging-controller": "^6.0.0",
"@metamask/network-controller": "^24.0.0"
Expand Down
224 changes: 224 additions & 0 deletions packages/signature-controller/src/SignatureController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,27 @@ const PERMIT_REQUEST_MOCK = {
traceContext: null,
};

const DELEGATION_PARAMS_MOCK = {
data: '{"types":{"EIP712Domain":[{"name":"chainId","type":"uint256"}],"Delegation":[{"name":"delegate","type":"address"},{"name":"delegator","type":"address"},{"name":"authority","type":"bytes"},{"name":"caveats","type":"bytes"}]},"primaryType":"Delegation","domain":{"chainId":1},"message":{"delegate":"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4","delegator":"0x975e73efb9ff52e23bac7f7e043a1ecd06d05477","authority":"0x1234abcd","caveats":[]},"metadata":{"origin":"https://metamask.github.io","justification":"Testing delegation"}}',
from: '0x975e73efb9ff52e23bac7f7e043a1ecd06d05477',
version: 'V4',
signatureMethod: 'eth_signTypedData_v4',
};

const DELEGATION_REQUEST_MOCK = {
method: 'eth_signTypedData_v4',
params: [
'0x975e73efb9ff52e23bac7f7e043a1ecd06d05477',
DELEGATION_PARAMS_MOCK.data,
],
jsonrpc: '2.0',
id: 1680528591,
origin: 'npm:@metamask/gator-permissions-snap',
networkClientId: 'mainnet',
tabId: 1048807182,
traceContext: null,
};

/**
* Create a mock messenger instance.
*
Expand All @@ -101,6 +122,7 @@ function createMessengerMock() {
const keyringControllerSignTypedMessageMock = jest.fn();
const loggingControllerAddMock = jest.fn();
const networkControllerGetNetworkClientByIdMock = jest.fn();
const decodePermissionFromPermissionContextForOriginMock = jest.fn();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const callMock = (method: string, ...args: any[]) => {
Expand All @@ -117,6 +139,8 @@ function createMessengerMock() {
return loggingControllerAddMock(...args);
case 'NetworkController:getNetworkClientById':
return networkControllerGetNetworkClientByIdMock(...args);
case 'GatorPermissionsController:decodePermissionFromPermissionContextForOrigin':
return decodePermissionFromPermissionContextForOriginMock(...args);
default:
throw new Error(`Messenger method not recognised: ${method}`);
}
Expand Down Expand Up @@ -144,12 +168,17 @@ function createMessengerMock() {
},
});

decodePermissionFromPermissionContextForOriginMock.mockReturnValue({
kind: 'decoded-permission',
});

return {
accountsControllerGetStateMock,
approvalControllerAddRequestMock,
keyringControllerSignPersonalMessageMock,
keyringControllerSignTypedMessageMock,
loggingControllerAddMock,
decodePermissionFromPermissionContextForOriginMock,
messenger,
};
}
Expand Down Expand Up @@ -933,6 +962,201 @@ describe('SignatureController', () => {
).toBe(SignTypedDataVersion.V3);
});

describe('delegations', () => {
it('invokes decodePermissionFromRequest to get execution permission', async () => {
const {
controller,
decodePermissionFromPermissionContextForOriginMock,
} = createController();

await controller.newUnsignedTypedMessage(
DELEGATION_PARAMS_MOCK,
DELEGATION_REQUEST_MOCK,
SignTypedDataVersion.V4,
{ parseJsonData: false },
);

expect(
decodePermissionFromPermissionContextForOriginMock,
).toHaveBeenCalledWith({
origin: 'npm:@metamask/gator-permissions-snap',
chainId: 1,
delegation: {
delegate: '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4',
delegator: '0x975e73efb9ff52e23bac7f7e043a1ecd06d05477',
authority: '0x1234abcd',
caveats: [],
},
metadata: {
origin: 'https://metamask.github.io',
justification: 'Testing delegation',
},
});
});

it('does not invoke decodePermissionFromRequest if version is not V4', async () => {
const {
controller,
decodePermissionFromPermissionContextForOriginMock,
} = createController();

await controller.newUnsignedTypedMessage(
DELEGATION_PARAMS_MOCK,
DELEGATION_REQUEST_MOCK,
SignTypedDataVersion.V3,
{ parseJsonData: false },
);

expect(
decodePermissionFromPermissionContextForOriginMock,
).not.toHaveBeenCalled();
});

it('sets decodedPermission on the message state', async () => {
const { controller } = createController();

await controller.newUnsignedTypedMessage(
DELEGATION_PARAMS_MOCK,
DELEGATION_REQUEST_MOCK,
SignTypedDataVersion.V4,
{ parseJsonData: false },
);

const { decodedPermission } =
controller.state.signatureRequests[ID_MOCK];

expect(decodedPermission).toStrictEqual({
kind: 'decoded-permission',
});
});

it('does not invoke decodePermissionFromRequest if data is not a delegation request', async () => {
const {
controller,
decodePermissionFromPermissionContextForOriginMock,
} = createController();

await controller.newUnsignedTypedMessage(
{
...DELEGATION_PARAMS_MOCK,
data: '{primaryType:"not-a-delegation"}',
},
DELEGATION_REQUEST_MOCK,
SignTypedDataVersion.V4,
{ parseJsonData: false },
);

expect(
decodePermissionFromPermissionContextForOriginMock,
).not.toHaveBeenCalled();
});

it('does not set decodedPermission if data is not a delegation request', async () => {
const { controller } = createController();

await controller.newUnsignedTypedMessage(
{
...DELEGATION_PARAMS_MOCK,
data: '{primaryType:"not-a-delegation"}',
},
DELEGATION_REQUEST_MOCK,
SignTypedDataVersion.V4,
{ parseJsonData: false },
);

const { decodedPermission } =
controller.state.signatureRequests[ID_MOCK];

expect(decodedPermission).toBeUndefined();
});

it('does not set decodedPermission if decoding throws an error', async () => {
const {
controller,
decodePermissionFromPermissionContextForOriginMock,
} = createController();

decodePermissionFromPermissionContextForOriginMock.mockImplementation(
() => {
throw new Error('An error occurred');
},
);

await controller.newUnsignedTypedMessage(
DELEGATION_PARAMS_MOCK,
DELEGATION_REQUEST_MOCK,
SignTypedDataVersion.V4,
{ parseJsonData: false },
);

const { decodedPermission } =
controller.state.signatureRequests[ID_MOCK];

expect(decodedPermission).toBeUndefined();
});

it('does not set decodedPermission if decoding returns undefined', async () => {
const {
controller,
decodePermissionFromPermissionContextForOriginMock,
} = createController();

decodePermissionFromPermissionContextForOriginMock.mockReturnValue(
undefined,
);

await controller.newUnsignedTypedMessage(
DELEGATION_PARAMS_MOCK,
DELEGATION_REQUEST_MOCK,
SignTypedDataVersion.V4,
{ parseJsonData: false },
);

const { decodedPermission } =
controller.state.signatureRequests[ID_MOCK];

expect(decodedPermission).toBeUndefined();
});

it('does not set decodedPermission if metadata is invalid', async () => {
const { controller } = createController();

const delegationParamsMock = {
...DELEGATION_PARAMS_MOCK,
data: {
...JSON.parse(DELEGATION_PARAMS_MOCK.data),
metadata: {},
},
};

const delegationRequestMock = {
method: 'eth_signTypedData_v4',
params: [
'0x975e73efb9ff52e23bac7f7e043a1ecd06d05477',
delegationParamsMock.data,
],
jsonrpc: '2.0',
id: 1680528591,
origin: 'npm:@metamask/gator-permissions-snap',
networkClientId: 'mainnet',
tabId: 1048807182,
traceContext: null,
};

await controller.newUnsignedTypedMessage(
delegationParamsMock,
delegationRequestMock,
SignTypedDataVersion.V4,
{ parseJsonData: false },
);

const { decodedPermission } =
controller.state.signatureRequests[ID_MOCK];

expect(decodedPermission).toBeUndefined();
});
});

describe('decodeSignature', () => {
it('invoke decodeSignature to get decoding data', async () => {
const MOCK_STATE_CHANGES = {
Expand Down
Loading
Loading