Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added multichain api notifications #25869

Merged
merged 41 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
70b2068
Added initial WIP
shanejonas Jul 16, 2024
9e8f0df
pushing WIP WIP
shanejonas Jul 16, 2024
e596389
fix: get it almost there
shanejonas Jul 16, 2024
02ebac6
lint
jiexi Jul 16, 2024
a1e4c9c
Merge branch 'caip-multichain' into sj/caip-multichain-notifications
jiexi Jul 16, 2024
d093eab
fix: issue with destroy
shanejonas Jul 17, 2024
f5fb9c7
fix: add tests for multichain subscription manage and middleware manager
shanejonas Jul 18, 2024
dc671f9
fix: wip
shanejonas Jul 23, 2024
cd6f10e
fix: wip
shanejonas Jul 23, 2024
c51e95f
fix: get destroy working and fix linting
shanejonas Jul 24, 2024
44e5f81
fix: remove console logs
shanejonas Jul 24, 2024
4ac28e5
Merge branch 'caip-multichain' into sj/caip-multichain-notifications
shanejonas Jul 24, 2024
52003d2
fix: remove hook
shanejonas Jul 24, 2024
12c15e2
fix: add nullish coalescing operating instead of ||
shanejonas Jul 29, 2024
adf042c
fix: NetworkController types
shanejonas Jul 29, 2024
c098ef4
fix: remove unused import
shanejonas Jul 29, 2024
2f39c90
fix: typings for onNotification + middleware destroy
shanejonas Jul 30, 2024
c547db3
fix: linting
shanejonas Jul 30, 2024
a5f73ec
fix: add notifications in stateChange instead of provider authorize
shanejonas Jul 30, 2024
7c48fab
Merge branch 'caip-multichain' into sj/caip-multichain-notifications
jiexi Jul 30, 2024
757a205
Fix
jiexi Jul 30, 2024
e126aab
Merge branch 'caip-multichain' into sj/caip-multichain-notifications
jiexi Jul 30, 2024
2e63b0f
fix: stateChange logic
shanejonas Jul 30, 2024
820abb9
fix: remove unused code
shanejonas Jul 30, 2024
aeee091
Merge branch 'caip-multichain' into sj/caip-multichain-notifications
jiexi Jul 30, 2024
581fe0c
fix: add more tests for MultichainMiddlewareManager and subscriptionM…
shanejonas Aug 1, 2024
a51fb95
fix: remove console log
shanejonas Aug 1, 2024
87c0017
fix: destroy
shanejonas Aug 1, 2024
b5cd7d9
use intersection type instead of union type for adding destroy to mid…
adonesky1 Aug 1, 2024
1a88f4e
fix: type for middleware
shanejonas Aug 2, 2024
5ceb061
fix: more typings
shanejonas Aug 2, 2024
0aa3d00
fix: linting
shanejonas Aug 2, 2024
3df485d
fix: remove unneeded typing
shanejonas Aug 2, 2024
2a5920c
Rename multichainSubscriptionManager.ts to MultichainSubscriptionMana…
shanejonas Aug 2, 2024
1147629
fix: convert to class and set middleware destroy in constructor
shanejonas Aug 2, 2024
f172d1b
fix: remove destroy typings from test
shanejonas Aug 2, 2024
77760fe
fix: add note about JsonRpcMiddleware type and give more descriptive …
shanejonas Aug 19, 2024
39db51e
fix: add tests and feedback for removed authorizations selector
shanejonas Aug 19, 2024
c22c29e
Merge branch 'caip-multichain' into sj/caip-multichain-notifications
shanejonas Aug 19, 2024
804a55b
fix: dedupe
shanejonas Aug 19, 2024
9b9b581
Update LavaMoat policies
metamaskbot Aug 19, 2024
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
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { JsonRpcMiddleware } from 'json-rpc-engine';
import MultichainSubscriptionManager, { createMultichainMiddlewareManager } from './multichainSubscriptionManager';

const newHeadsNotificationMock = {
method: 'eth_subscription',
params: {
result: {
difficulty: '0x15d9223a23aa',
extraData: '0xd983010305844765746887676f312e342e328777696e646f7773',
gasLimit: '0x47e7c4',
gasUsed: '0x38658',
logsBloom:
'0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
miner: '0xf8b483dba2c3b7176a3da549ad41a48bb3121069',
nonce: '0x084149998194cc5f',
number: '0x1348c9',
parentHash:
'0x7736fab79e05dc611604d22470dadad26f56fe494421b5b333de816ce1f25701',
receiptRoot:
'0x2fab35823ad00c7bb388595cb46652fe7886e00660a01e867824d3dceb1c8d36',
sha3Uncles:
'0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
stateRoot:
'0xb3346685172db67de536d8765c43c31009d0eb3bd9c501c9be3229203f15f378',
timestamp: '0x56ffeff8',
},
},
};

describe('multichainSubscriptionManager', () => {
it('should subscribe to a chain', (done) => {
const scope = 'eip155:1';
const mockFindNetworkClientIdByChainId = jest.fn()
const mockGetNetworkClientById = jest.fn().mockImplementation(() => ({
blockTracker: {},
provider: {},
}));
const subscriptionManager = new MultichainSubscriptionManager({
findNetworkClientIdByChainId: mockFindNetworkClientIdByChainId,
getNetworkClientById: mockGetNetworkClientById,
});
subscriptionManager.subscribe(scope);
subscriptionManager.on('notification', (notification: any) => {
expect(notification).toMatchObject({
method: "wallet_invokeMethod",
params: {
scope,
request: newHeadsNotificationMock,
}
});
done();
});
subscriptionManager.onNotification(scope, newHeadsNotificationMock);
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
});
});
describe('multichainMiddlewareManager', () => {
it('should add middleware and get called for the scope', () => {
const multichainMiddlewareManager = createMultichainMiddlewareManager();
const middlewareSpy = jest.fn();
multichainMiddlewareManager.addMiddleware('eip155:1', middlewareSpy);
multichainMiddlewareManager.middleware({scope: 'eip155:1'}, {}, () => { }, () => {});
expect(middlewareSpy).toHaveBeenCalled();
})
it('should remove middleware', () => {
const multichainMiddlewareManager = createMultichainMiddlewareManager();
const middlewareMock = jest.fn();
(middlewareMock as unknown as {destroy: () => void}).destroy = jest.fn();
const scope = 'eip155:1';
multichainMiddlewareManager.addMiddleware(scope, middlewareMock);
multichainMiddlewareManager.removeMiddleware(scope);
expect(() => {
multichainMiddlewareManager.middleware({scope}, {}, () => { }, () => {});
}).toThrow();
})
});
109 changes: 109 additions & 0 deletions app/scripts/lib/multichain-api/multichainSubscriptionManager.ts
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
NetworkControllerFindNetworkClientIdByChainIdAction,
NetworkControllerGetNetworkClientByIdAction,
} from '@metamask/network-controller';
import SafeEventEmitter from '@metamask/safe-event-emitter';
import { parseCaipChainId } from '@metamask/utils';
import { toHex } from '@metamask/controller-utils';
import { Scope } from './scope';

const createSubscriptionManager = require('@metamask/eth-json-rpc-filters/subscriptionManager');
Copy link
Contributor

Choose a reason for hiding this comment

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

what's the issue with using import? types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yea theres no types available for this package


type MultichainSubscriptionManagerOptions = {
findNetworkClientIdByChainId: NetworkControllerFindNetworkClientIdByChainIdAction['handler'];
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
getNetworkClientById: NetworkControllerGetNetworkClientByIdAction['handler'];
};

export default class MultichainSubscriptionManager extends SafeEventEmitter {
private subscriptionsByChain: { [scope: string]: unknown };

private findNetworkClientIdByChainId: NetworkControllerFindNetworkClientIdByChainIdAction['handler'];

private getNetworkClientById: NetworkControllerGetNetworkClientByIdAction['handler'];
shanejonas marked this conversation as resolved.
Show resolved Hide resolved

private subscriptionManagerByChain: { [scope: string]: any };

constructor(options: MultichainSubscriptionManagerOptions) {
super();
this.findNetworkClientIdByChainId = options.findNetworkClientIdByChainId;
this.getNetworkClientById = options.getNetworkClientById;
this.subscriptionManagerByChain = {};
this.subscriptionsByChain = {};
}

onNotification(scope: Scope, message: any) {
this.emit('notification', {
method: 'wallet_invokeMethod',
params: {
scope,
request: message,
},
});
}

subscribe(scope: Scope) {
const networkClientId = this.findNetworkClientIdByChainId(
toHex(parseCaipChainId(scope).reference),
);
const networkClient = this.getNetworkClientById(networkClientId);
const subscriptionManager = createSubscriptionManager({
blockTracker: networkClient.blockTracker,
provider: networkClient.provider,
});
this.subscriptionManagerByChain[scope] = subscriptionManager;
this.subscriptionsByChain[scope] = this.onNotification.bind(this, scope);
subscriptionManager.events.on(
'notification',
this.subscriptionsByChain[scope],
);
return subscriptionManager;
}

unsubscribe(scope: Scope) {
const subscriptionManager = this.subscriptionManagerByChain[scope];
if (subscriptionManager) {
subscriptionManager.events.off(
'notification',
this.subscriptionsByChain[scope],
);
subscriptionManager.destroy();
}
delete this.subscriptionManagerByChain[scope];
delete this.subscriptionsByChain[scope];
}

unsubscribeAll() {
Object.keys(this.subscriptionManagerByChain).forEach((scope) => {
this.unsubscribe(scope);
});
}
}

// per scope middleware to handle legacy middleware
export const createMultichainMiddlewareManager = () => {
const middlewaresByScope: Record<Scope, any> = {};
const removeMiddleware = (scope: Scope) => {
const middleware = middlewaresByScope[scope];
middleware.destroy();
delete middlewaresByScope[scope];
};

const removeAllMiddleware = () => {
Object.keys(middlewaresByScope).forEach((scope) => {
removeMiddleware(scope);
});
};

const addMiddleware = (scope: Scope, middleware: any) => {
middlewaresByScope[scope] = middleware;
};

return {
middleware: (req: any, res: any, next: any, end: any) => {
return middlewaresByScope[req.scope](req, res, next, end);
},
addMiddleware,
removeMiddleware,
removeAllMiddleware,
};
};
31 changes: 27 additions & 4 deletions app/scripts/lib/multichain-api/provider-authorize.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export async function providerAuthorizeHandler(req, res, _next, end, hooks) {
),
);
}
console.log(' got to before sessionid');
shanejonas marked this conversation as resolved.
Show resolved Hide resolved

const sessionId = '0xdeadbeef';

Expand All @@ -72,6 +73,7 @@ export async function providerAuthorizeHandler(req, res, _next, end, hooks) {
[RestrictedMethods.eth_accounts]: {},
},
);
console.log(' got to after requestPermissions');
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
const permittedAccounts = getAccountsFromPermission(subjectPermission);
const { flattenedRequiredScopes, flattenedOptionalScopes } = processScopes(
requiredScopes,
Expand Down Expand Up @@ -113,14 +115,35 @@ export async function providerAuthorizeHandler(req, res, _next, end, hooks) {
},
});

const mergedScopes = mergeScopes(
flattenedRequiredScopes,
flattenedOptionalScopes,
);

// clear per scope middleware
hooks.multichainMiddlewareManager.removeAllMiddleware();
hooks.multichainSubscriptionManager.unsubscribeAll();
shanejonas marked this conversation as resolved.
Show resolved Hide resolved

// if the eth_subscription notification is in the scope and eth_subscribe is in the methods
// then get the subscriptionManager going for that scope
Object.entries(mergedScopes).forEach(([scope, scopeObject]) => {
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
if (
scopeObject.notifications.includes('eth_subscription') &&
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
scopeObject.methods.includes('eth_subscribe')
) {
const subscriptionManager = hooks.subscriptionManager.subscribe(scope);
hooks.multichainMiddlewareManager.addMiddleware(
scope,
subscriptionManager.middleware,
);
}
});

// TODO: metrics/tracking after approval

res.result = {
sessionId,
sessionScopes: mergeScopes(
flattenedRequiredScopes,
flattenedOptionalScopes,
),
sessionScopes: mergedScopes,
sessionProperties,
};
return end();
Expand Down
11 changes: 11 additions & 0 deletions app/scripts/lib/multichain-api/provider-authorize.test.js
Copy link
Contributor

Choose a reason for hiding this comment

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

should add scenarios to cover this new feature

Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ const createMockedHandler = () => {
const response = {};
const handler = (request) =>
providerAuthorizeHandler(request, response, next, end, {
multichainMiddlewareManager: {
addMiddleware: jest.fn(),
removeMiddleware: jest.fn(),
removeAllMiddleware: jest.fn(),
},
multichainSubscriptionManager: {
subscribe: jest.fn(),
unsubscribe: jest.fn(),
unsubscribeAll: jest.fn(),
},
findNetworkClientIdByChainId,
requestPermissions,
grantPermissions,
Expand Down Expand Up @@ -295,6 +305,7 @@ describe('provider_authorize', () => {
},
});
await handler(baseRequest);
console.log('RESPONSE ===', response);
shanejonas marked this conversation as resolved.
Show resolved Hide resolved

expect(response.result).toStrictEqual({
sessionId: '0xdeadbeef',
Expand Down
1 change: 1 addition & 0 deletions app/scripts/lib/multichain-api/provider-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export async function providerRequestHandler(
}

Object.assign(request, {
scope,
networkClientId,
method: wrappedRequest.method,
params: wrappedRequest.params,
Expand Down
2 changes: 2 additions & 0 deletions app/scripts/lib/multichain-api/provider-request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ describe('provider_request', () => {

await handler(request);
expect(request).toStrictEqual({
scope: 'eip155:1',
origin: 'http://test.com',
networkClientId: 'mainnet',
method: 'eth_call',
Expand Down Expand Up @@ -223,6 +224,7 @@ describe('provider_request', () => {
};
await handler(walletRequest);
expect(walletRequest).toStrictEqual({
scope: 'wallet',
origin: 'http://test.com',
networkClientId: 'selectedNetworkClientId',
method: 'wallet_watchAsset',
Expand Down
38 changes: 33 additions & 5 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,9 @@ import {
} from './lib/multichain-api/caip25permissions';
import { multichainMethodCallValidatorMiddleware } from './lib/multichain-api/multichainMethodCallValidator';
import { decodeTransactionData } from './lib/transaction/decode/util';
import MultichainSubscriptionManager, {
createMultichainMiddlewareManager,
} from './lib/multichain-api/multichainSubscriptionManager';

export const METAMASK_CONTROLLER_EVENTS = {
// Fired after state changes that impact the extension badge (unapproved msg count)
Expand Down Expand Up @@ -532,6 +535,19 @@ export default class MetamaskController extends EventEmitter {
trackMetaMetricsEvent: (...args) =>
this.metaMetricsController.trackEvent(...args),
});

this.multichainSubscriptionManager = new MultichainSubscriptionManager({
getNetworkClientById: this.networkController.getNetworkClientById.bind(
this.networkController,
),
findNetworkClientIdByChainId:
this.networkController.findNetworkClientIdByChainId.bind(
this.networkController,
),
});

this.multichainMiddlewareManager = createMultichainMiddlewareManager();
shanejonas marked this conversation as resolved.
Show resolved Hide resolved

this.networkController.initializeProvider();
this.provider =
this.networkController.getProviderAndBlockTracker().provider;
Expand Down Expand Up @@ -4551,6 +4567,10 @@ export default class MetamaskController extends EventEmitter {
config.type !== networkConfigurationId,
);

const scope = `eip155:${parseInt(chainId, 16)}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we have a CAIP formatting helper somewhere to do this?

this.multichainSubscriptionManager.unsubscribe(scope);
this.multichainMiddlewareManager.removeMiddleware(scope);
shanejonas marked this conversation as resolved.
Show resolved Hide resolved

// if this network configuration is only one for a given chainId
// remove all permissions for that chainId
if (!hasOtherConfigsForChainId) {
Expand Down Expand Up @@ -5602,6 +5622,8 @@ export default class MetamaskController extends EventEmitter {
createScaffoldMiddleware({
[MESSAGE_TYPE.PROVIDER_AUTHORIZE]: (request, response, next, end) => {
return providerAuthorizeHandler(request, response, next, end, {
multichainMiddlewareManager: this.multichainMiddlewareManager,
multichainSubscriptionManager: this.multichainSubscriptionManager,
grantPermissions: this.permissionController.grantPermissions.bind(
this.permissionController,
),
Expand Down Expand Up @@ -5720,11 +5742,11 @@ export default class MetamaskController extends EventEmitter {
},
),
// TODO remove this hook
requestPermissionsForOrigin:
this.permissionController.requestPermissions.bind(
this.permissionController,
{ origin },
),
// requestPermissionsForOrigin:
// this.permissionController.requestPermissions.bind(
// this.permissionController,
// { origin },
// ),
getCaveat: ({ target, caveatType }) => {
try {
return this.permissionController.getCaveat(
Expand Down Expand Up @@ -5795,6 +5817,12 @@ export default class MetamaskController extends EventEmitter {

engine.push(this.metamaskMiddleware);

this.multichainSubscriptionManager.on('notification', (message) =>
engine.emit('notification', message),
);

engine.push(this.multichainMiddlewareManager.middleware);

engine.push((req, res, _next, end) => {
const { provider } = this.networkController.getNetworkClientById(
req.networkClientId,
Expand Down