Skip to content

Commit 5f7cf82

Browse files
add(SDKConnectV2) - Scaffolding (MetaMask#18305)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR scaffolds the architecture for the new **Mobile Wallet Protocol**, which will be housed within the `core/SDKConnectV2/` directory. The long-term goal is to replace the legacy `SDKConnect` system with a modern, robust, and multichain-first protocol. This PR represents the first step in a series of planned implementations. Specifically, this PR: * Creates the `core/SDKConnectV2/` directory structure. * Introduces the central orchestrator service, `ConnectionRegistry`. * Defines the key decoupling interface, `IHostApplicationAdapter`. * Makes a minimal, surgical modification to `handleMetaMaskDeeplink.ts` to identify and route `v=2` deeplinks to the new `ConnectionRegistry`. There are no user-facing changes in this pull request. Its sole purpose is to lay the groundwork and validate the control flow redirection from the legacy system to the new V2 module. **Upcoming pull-requests will be broken by business logic:** 1. Connection handling 2. Message handling 3. Disconnect 4. Connection resumption ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** None. Scaffolding only. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: Alex Donesky <adonesky@gmail.com>
1 parent 51c3c41 commit 5f7cf82

17 files changed

Lines changed: 454 additions & 14 deletions

app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Device from '../../../util/device';
55
import AppConstants from '../../AppConstants';
66
import handleDeeplink from '../../SDKConnect/handlers/handleDeeplink';
77
import SDKConnect from '../../SDKConnect/SDKConnect';
8+
import SDKConnectV2 from '../../SDKConnectV2';
89
import WC2Manager from '../../WalletConnect/WalletConnectV2';
910
import DeeplinkManager from '../DeeplinkManager';
1011
import extractURLParams from './extractURLParams';
@@ -13,6 +14,7 @@ import handleMetaMaskDeeplink from './handleMetaMaskDeeplink';
1314
jest.mock('../../../core/AppConstants');
1415
jest.mock('../../../core/SDKConnect/handlers/handleDeeplink');
1516
jest.mock('../../../core/SDKConnect/SDKConnect');
17+
jest.mock('../../../core/SDKConnectV2');
1618
jest.mock('../../../core/WalletConnect/WalletConnectV2');
1719
jest.mock('../../../core/NativeModules', () => ({
1820
Minimizer: {
@@ -110,6 +112,30 @@ describe('handleMetaMaskProtocol', () => {
110112
expect(handled).toHaveBeenCalled();
111113
});
112114

115+
describe('when url starts with ${PREFIXES.METAMASK}${ACTIONS.CONNECT}/mwp', () => {
116+
const spyHandleConnectDeeplink = jest.spyOn(
117+
SDKConnectV2,
118+
'handleConnectDeeplink',
119+
);
120+
beforeEach(() => {
121+
url = `${PREFIXES.METAMASK}${ACTIONS.CONNECT}/mwp`;
122+
spyHandleConnectDeeplink.mockImplementation(jest.fn());
123+
});
124+
125+
it('should call SDKConnectV2.handleConnectDeeplink', () => {
126+
handleMetaMaskDeeplink({
127+
instance,
128+
handled,
129+
params,
130+
url,
131+
origin,
132+
wcURL,
133+
});
134+
135+
expect(spyHandleConnectDeeplink).toHaveBeenCalledWith(url);
136+
});
137+
});
138+
113139
describe('when url starts with ${PREFIXES.METAMASK}${ACTIONS.ANDROID_SDK}', () => {
114140
beforeEach(() => {
115141
url = `${PREFIXES.METAMASK}${ACTIONS.ANDROID_SDK}`;

app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import WC2Manager from '../../WalletConnect/WalletConnectV2';
1010
import DeeplinkManager from '../DeeplinkManager';
1111
import parseOriginatorInfo from '../parseOriginatorInfo';
1212
import extractURLParams from './extractURLParams';
13+
import SDKConnectV2 from '../../SDKConnectV2';
14+
1315
export function handleMetaMaskDeeplink({
1416
instance,
1517
handled,
@@ -26,6 +28,7 @@ export function handleMetaMaskDeeplink({
2628
url: string;
2729
}) {
2830
handled();
31+
2932
if (url.startsWith(`${PREFIXES.METAMASK}${ACTIONS.ANDROID_SDK}`)) {
3033
DevLogger.log(
3134
`DeeplinkManager:: metamask launched via android sdk deeplink`,
@@ -38,6 +41,15 @@ export function handleMetaMaskDeeplink({
3841
return;
3942
}
4043

44+
if (url.startsWith(`${PREFIXES.METAMASK}${ACTIONS.CONNECT}/mwp`)) {
45+
DevLogger.log(
46+
`DeeplinkManager:: Mobile Wallet Protocol deeplink detected. Routing to SDKConnectV2.`,
47+
url,
48+
);
49+
SDKConnectV2.handleConnectDeeplink(url);
50+
return;
51+
}
52+
4153
if (url.startsWith(`${PREFIXES.METAMASK}${ACTIONS.CONNECT}`)) {
4254
if (params.redirect && origin === AppConstants.DEEPLINKS.ORIGIN_DEEPLINK) {
4355
SDKConnect.getInstance().state.navigation?.navigate(
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { HostApplicationAdapter } from './host-application-adapter';
2+
3+
describe('HostApplicationAdapter', () => {
4+
let adapter: HostApplicationAdapter;
5+
6+
beforeEach(() => {
7+
adapter = new HostApplicationAdapter();
8+
});
9+
10+
it('dummy tests for scaffolding, will be replaced with real tests', () => {
11+
expect(adapter).toBeDefined();
12+
expect(
13+
adapter.showConnectionApproval('test-id', {
14+
name: 'test-dapp-name',
15+
url: 'test-dapp-url',
16+
icon: 'test-dapp-icon',
17+
}),
18+
).resolves.not.toThrow();
19+
expect(() => adapter.showLoading()).not.toThrow();
20+
expect(() => adapter.hideLoading()).not.toThrow();
21+
expect(adapter.showOTPModal()).resolves.not.toThrow();
22+
expect(() => adapter.syncConnectionList([])).not.toThrow();
23+
});
24+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Connection } from '../types/connection';
2+
import { DappMetadata } from '../types/dapp-metadata';
3+
import { IHostApplicationAdapter } from '../types/host-application-adapter';
4+
5+
export class HostApplicationAdapter implements IHostApplicationAdapter {
6+
showConnectionApproval(
7+
_connectionId: string,
8+
_dappMetadata: DappMetadata,
9+
): Promise<void> {
10+
console.warn(
11+
'[SDKConnectV2] HostApplicationAdapter.showConnectionApproval called but is not yet implemented.',
12+
);
13+
return Promise.resolve();
14+
}
15+
16+
showLoading(): void {
17+
console.warn(
18+
'[SDKConnectV2] HostApplicationAdapter.showLoading called but is not yet implemented.',
19+
);
20+
}
21+
22+
hideLoading(): void {
23+
console.warn(
24+
'[SDKConnectV2] HostApplicationAdapter.hideLoading called but is not yet implemented.',
25+
);
26+
}
27+
28+
showOTPModal(): Promise<void> {
29+
console.warn(
30+
'[SDKConnectV2] HostApplicationAdapter.showOTPModal called but is not yet implemented.',
31+
);
32+
return Promise.resolve();
33+
}
34+
35+
syncConnectionList(_connections: Connection[]): void {
36+
console.warn(
37+
'[SDKConnectV2] HostApplicationAdapter.syncConnectionList called but is not yet implemented.',
38+
);
39+
}
40+
}

app/core/SDKConnectV2/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { HostApplicationAdapter } from './adapters/host-application-adapter';
2+
import { ConnectionStore } from './store/connection-store';
3+
import { ConnectionRegistry } from './services/connection-registry';
4+
5+
const hostapp = new HostApplicationAdapter();
6+
const store = new ConnectionStore();
7+
const registry = new ConnectionRegistry(hostapp, store);
8+
9+
export default registry;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ConnectionRegistry } from './connection-registry';
2+
import { HostApplicationAdapter } from '../adapters/host-application-adapter';
3+
import { ConnectionStore } from '../store/connection-store';
4+
5+
describe('ConnectionRegistry', () => {
6+
let registry: ConnectionRegistry;
7+
8+
beforeEach(() => {
9+
const hostapp = new HostApplicationAdapter();
10+
const store = new ConnectionStore();
11+
registry = new ConnectionRegistry(hostapp, store);
12+
});
13+
14+
it('dummy tests for scaffolding, will be replaced with real tests', () => {
15+
expect(registry).toBeDefined();
16+
expect(() => registry.handleConnectDeeplink('test-deeplink')).not.toThrow();
17+
});
18+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { IHostApplicationAdapter } from '../types/host-application-adapter';
2+
import { IConnectionStore } from '../types/connection-store';
3+
4+
/**
5+
* The ConnectionRegistry is the central service responsible for managing the
6+
* lifecycle of all SDKConnectV2 connections. It acts as the primary entry
7+
* point and orchestrator for creating, managing, and tearing down secure
8+
* dApp sessions.
9+
*/
10+
export class ConnectionRegistry {
11+
private readonly hostapp: IHostApplicationAdapter;
12+
private readonly store: IConnectionStore;
13+
14+
/**
15+
* The constructor for the ConnectionRegistry.
16+
*
17+
* @param hostapp - An adapter that provides a bridge to the
18+
* host MetaMask Mobile application's UI and state management systems.
19+
* @param store - An adapter for the persistence layer, used to
20+
* save and retrieve connection data.
21+
*/
22+
constructor(hostapp: IHostApplicationAdapter, store: IConnectionStore) {
23+
this.hostapp = hostapp;
24+
this.store = store;
25+
}
26+
27+
/**
28+
* The primary entry point for handling a new connection request from a
29+
* v2 deeplink (e.g., from a QR code).
30+
* @param url - The full deeplink URL that triggered the connection.
31+
*/
32+
public handleConnectDeeplink(url: string): void {
33+
console.warn(
34+
'[SDKConnectV2] ConnectionRegistry: handleConnectDeeplink successfully called with URL:',
35+
url,
36+
);
37+
38+
// In future, this method will be responsible for:
39+
// 1. Parsing the URL to extract the ConnectionRequest.
40+
// 2. Calling the hostapp to show the connection approval UI.
41+
// 3. Initiating the cryptographic handshake upon user approval.
42+
// 4. Saving the resulting connection to the store.
43+
}
44+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ConnectionStore } from './connection-store';
2+
import { Connection } from '../types/connection';
3+
4+
describe('ConnectionStore', () => {
5+
let store: ConnectionStore;
6+
7+
beforeEach(() => {
8+
store = new ConnectionStore();
9+
});
10+
11+
it('dummy tests for scaffolding, will be replaced with real tests', () => {
12+
expect(store).toBeDefined();
13+
expect(store.save({} as Connection)).resolves.not.toThrow();
14+
expect(store.get('test-id')).resolves.toBeNull();
15+
expect(store.list()).resolves.toEqual([]);
16+
expect(store.delete('test-id')).resolves.not.toThrow();
17+
});
18+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Connection } from '../types/connection';
2+
import { IConnectionStore } from '../types/connection-store';
3+
4+
/**
5+
* Placeholder implementation of the IConnectionStore.
6+
* For now, this class provides no-op implementations of the
7+
* required storage methods to satisfy the dependency requirements
8+
* of the ConnectionRegistry.
9+
*/
10+
export class ConnectionStore implements IConnectionStore {
11+
save(_connection: Connection): Promise<void> {
12+
console.warn(
13+
'[SDKConnectV2] ConnectionStore.save called but is not yet implemented.',
14+
);
15+
return Promise.resolve();
16+
}
17+
18+
get(_id: string): Promise<Connection | null> {
19+
console.warn(
20+
'[SDKConnectV2] ConnectionStore.get called but is not yet implemented.',
21+
);
22+
return Promise.resolve(null);
23+
}
24+
25+
list(): Promise<Connection[]> {
26+
console.warn(
27+
'[SDKConnectV2] ConnectionStore.list called but is not yet implemented.',
28+
);
29+
return Promise.resolve([]);
30+
}
31+
32+
delete(_id: string): Promise<void> {
33+
console.warn(
34+
'[SDKConnectV2] ConnectionStore.delete called but is not yet implemented.',
35+
);
36+
return Promise.resolve();
37+
}
38+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { SessionRequest } from '@metamask/mobile-wallet-protocol-core';
2+
import { DappMetadata } from './dapp-metadata';
3+
4+
/**
5+
* Represents an incoming connection request parsed from a QR code or deep link.
6+
* This is the shared data contract between the dApp SDK and the mobile wallet,
7+
* as defined in the technical proposal. It encapsulates all the information
8+
* needed to initiate a new session.
9+
*/
10+
export interface ConnectionRequest {
11+
/**
12+
* The low-level protocol session request, containing cryptographic
13+
* and channel information.
14+
*/
15+
sessionRequest: SessionRequest;
16+
17+
/**
18+
* Metadata about the decentralized application (dApp) that is
19+
* requesting the connection.
20+
*/
21+
dappMetadata: DappMetadata;
22+
}

0 commit comments

Comments
 (0)