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
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"packages/playground-next",
"packages/sdk-multichain",
"playground/multichain-node",
"playground/multichain-react"
"playground/multichain-react",
"playground/multichain-react-native"
],
"scripts": {
"build": "yarn install && cd packages/sdk-socket-server-next && yarn install && cd ../.. && yarn workspaces foreach --verbose run build:pre-tsc && yarn workspaces foreach --verbose --topological --parallel --no-private run build && yarn workspaces foreach --verbose run build:post-tsc ",
Expand Down Expand Up @@ -114,8 +115,6 @@
"ts-node": "^10.9.1"
},
"resolutions": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"secp256k1": "^5.0.1",
"elliptic": "6.6.1"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-multichain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
},
"license": "MIT",
"dependencies": {
"@metamask/mobile-wallet-protocol-core": "^0.2.0",
"@metamask/mobile-wallet-protocol-core": "^0.3.0",
"@metamask/mobile-wallet-protocol-dapp-client": "^0.2.1",
"@metamask/multichain-api-client": "^0.8.0",
"@metamask/onboarding": "^1.0.1",
Expand Down
35 changes: 0 additions & 35 deletions packages/sdk-multichain/src/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,41 +269,6 @@ function testSuite<T extends MultichainOptions>({ platform, createSDK, options:
t.expect(sdk.state === 'disconnected').toBe(true);
});

t.it(`${platform} should handle session revocation errors on session upgrade`, async () => {
const existingSessionData: SessionData = {
...mockSessionData,
sessionScopes: {
'eip155:1': {
methods: ['eth_sendTransaction'],
notifications: ['accountsChanged'],
accounts: ['eip155:1:0x1234567890abcdef1234567890abcdef12345678'],
},
},
};

const revocationError = new Error('Failed to revoke session');

mockedData.mockWalletCreateSession.mockResolvedValue(existingSessionData);
mockedData.mockWalletGetSession.mockResolvedValue(existingSessionData);
mockedData.mockWalletRevokeSession.mockImplementation(async () => {
throw revocationError;
});
t.vi.spyOn(SessionStore.prototype, 'list').mockImplementation(async () => Promise.resolve([await (mockedData as any).mockWalletGetSession()]));

const scopes = ['eip155:137'] as Scope[]; // Different scope to trigger upgrade
const caipAccountIds = ['eip155:137:0x1234567890abcdef1234567890abcdef12345678'] as any;

mockedData.nativeStorageStub.setItem('multichain-transport', transportString);
sdk = await createSDK(testOptions);

t.expect(sdk.state).toBe('connected');
t.expect(sdk.storage).toBeDefined();
t.expect(() => sdk.provider).toThrow();
t.expect(sdk.transport).toBeDefined();

await t.expect(sdk.connect(scopes, caipAccountIds)).rejects.toThrow(revocationError);
});

t.it(`${platform} should disconnect transport successfully`, async () => {
mockedData.mockWalletGetSession.mockResolvedValue(mockSessionData);
mockedData.nativeStorageStub.setItem('multichain-transport', transportString);
Expand Down
4 changes: 4 additions & 0 deletions packages/sdk-multichain/src/domain/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export abstract class Modal<Options, Data extends DataType = DataType> {

constructor(protected readonly options: Options) {}

get isMounted() {
return this.instance !== undefined;
}

get data() {
if (typeof this.options === 'object' && this.options && 'link' in this.options) {
return this.options.link as Data;
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-multichain/src/invoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ function testSuite<T extends MultichainOptions>({ platform, createSDK, options:
t.expect(sdk.state).toBe('connected');
t.expect(() => sdk.provider).toThrow();

await t.expect(sdk.invokeMethod(options)).rejects.toThrow('Failed to invoke method');
await t.expect(sdk.invokeMethod(options)).rejects.toThrow('RPCErr53: RPC Client invoke method reason (Failed to invoke method)');
});
});
}
Expand Down
168 changes: 78 additions & 90 deletions packages/sdk-multichain/src/multichain/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
/** biome-ignore-all lint/suspicious/noAsyncPromiseExecutor: <explanation> */

import { ErrorCode, ProtocolError, type SessionRequest, SessionStore, WebSocketTransport } from '@metamask/mobile-wallet-protocol-core';
import { DappClient } from '@metamask/mobile-wallet-protocol-dapp-client';
import { getMultichainClient, type MultichainApiClient, type SessionData } from '@metamask/multichain-api-client';
import { analytics } from '@metamask/sdk-analytics';
import type { CaipAccountId, Json } from '@metamask/utils';
import { METAMASK_CONNECT_BASE_URL, METAMASK_DEEPLINK_BASE, MWP_RELAY_URL } from 'src/config';
import packageJson from '../../package.json';
import { type InvokeMethodOptions, type MultichainOptions, type RPCAPI, type Scope, TransportType } from '../domain';
import { createLogger, enableDebug, isEnabled as isLoggerEnabled } from '../domain/logger';
import { type ConnectionRequest, type ExtendedTransport, MultichainCore, type SDKState } from '../domain/multichain';
import { getPlatformType, hasExtension, isSecure, PlatformType } from '../domain/platform';
import { RPCClient } from './rpc/client';
import { getDappId, getVersion, setupDappMetadata, setupInfuraProvider } from './utils';
import { ErrorCode, ProtocolError, type SessionRequest, SessionStore, WebSocketTransport } from '@metamask/mobile-wallet-protocol-core';
import { METAMASK_CONNECT_BASE_URL, METAMASK_DEEPLINK_BASE, MWP_RELAY_URL } from 'src/config';
import { DappClient } from '@metamask/mobile-wallet-protocol-dapp-client';
import { DefaultTransport } from './transports/default';

import { MWPTransport } from './transports/mwp';
import { keymanager } from './transports/mwp/KeyManager';
import { DefaultTransport } from './transports/default';
import { getDappId, getVersion, openDeeplink, setupDappMetadata, setupInfuraProvider } from './utils';

//ENFORCE NAMESPACE THAT CAN BE DISABLED
const logger = createLogger('metamask-sdk:core');
Expand Down Expand Up @@ -153,6 +155,7 @@ export class MultichainSDK extends MultichainCore {
this.listener = apiTransport.onNotification(this.onTransportNotification.bind(this));
return apiTransport;
}

await this.storage.removeTransport();
}

Expand All @@ -167,6 +170,11 @@ export class MultichainSDK extends MultichainCore {
await this.transport.connect();
}
this.state = 'connected';
if (this.transport instanceof MWPTransport) {
await this.storage.setTransport(TransportType.MPW);
} else {
await this.storage.setTransport(TransportType.Browser);
}
} else {
this.state = 'loaded';
}
Expand Down Expand Up @@ -214,50 +222,24 @@ export class MultichainSDK extends MultichainCore {
await this.storage.setTransport(TransportType.MPW);
}

private openDeeplink(deeplink: string, universalLink: string) {
const { mobile } = this.options;
const useDeeplink = mobile && mobile.useDeeplink !== undefined ? mobile.useDeeplink : true;
if (useDeeplink) {
if (typeof window !== 'undefined') {
// We don't need to open a deeplink in a new tab
// It avoid the browser to display a blank page
window.location.href = deeplink;
}
} else if (typeof document !== 'undefined') {
// Workaround for https://github.com/rainbow-me/rainbowkit/issues/524.
// Using 'window.open' causes issues on iOS in non-Safari browsers and
// WebViews where a blank tab is left behind after connecting.
// This is especially bad in some WebView scenarios (e.g. following a
// link from Twitter) where the user doesn't have any mechanism for
// closing the blank tab.
// For whatever reason, links with a target of "_blank" don't suffer
// from this problem, and programmatically clicking a detached link
// element with the same attributes also avoids the issue.
const link = document.createElement('a');
link.href = universalLink;
link.target = '_self';
link.rel = 'noreferrer noopener';
link.click();
}
}

private async onBeforeUnload() {
if (this.options.ui.factory.modal) {
//Modal is still visible, remove storage to prevent glitch with "connecting" state
//Fixes glitch with "connecting" state when modal is still visible and we close screen or refresh
if (this.options.ui.factory.modal?.isMounted) {
await this.storage.removeTransport();
}
}

private createBeforeUnloadListener() {
if (typeof window !== 'undefined') {
if (typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined') {
window.addEventListener('beforeunload', this.onBeforeUnload.bind(this));
}
return () => {
if (typeof window !== 'undefined') {
if (typeof window !== 'undefined' && typeof window.removeEventListener !== 'undefined') {
window.removeEventListener('beforeunload', this.onBeforeUnload.bind(this));
}
};
}

private async showInstallModal(desktopPreferred: boolean, scopes: Scope[], caipAccountIds: CaipAccountId[]) {
// create the listener only once to avoid memory leaks
this.__beforeUnloadListener ??= this.createBeforeUnloadListener();
Expand Down Expand Up @@ -289,6 +271,7 @@ export class MultichainSDK extends MultichainCore {
this.options.ui.factory.unload();
this.options.ui.factory.modal?.unmount();
this.state = 'connected';
return this.storage.setTransport(TransportType.MPW);
})
.catch((err) => {
if (err instanceof ProtocolError) {
Expand Down Expand Up @@ -328,7 +311,7 @@ export class MultichainSDK extends MultichainCore {
}

private async deeplinkConnect(scopes: Scope[], caipAccountIds: CaipAccountId[]) {
return new Promise<void>((resolve, reject) => {
return new Promise<void>(async (resolve, reject) => {
this.dappClient.on('message', (payload: any) => {
const data = payload.data as Record<string, unknown>;
if (typeof data === 'object' && data !== null) {
Expand All @@ -348,26 +331,50 @@ export class MultichainSDK extends MultichainCore {
}
}
});
this.dappClient.once('session_request', (sessionRequest: SessionRequest) => {
const connectionRequest = {
sessionRequest,
metadata: {
dapp: this.options.dapp,
sdk: {
version: getVersion(),
platform: getPlatformType(),

let timeout: NodeJS.Timeout | undefined;

if (!this.transport.isConnected()) {
this.dappClient.once('session_request', (sessionRequest: SessionRequest) => {
const connectionRequest = {
sessionRequest,
metadata: {
dapp: this.options.dapp,
sdk: { version: getVersion(), platform: getPlatformType() },
},
},
};
const deeplink = this.options.ui.factory.createDeeplink(connectionRequest);
const universalLink = this.options.ui.factory.createUniversalLink(connectionRequest);
if (this.options.mobile?.preferredOpenLink) {
this.options.mobile.preferredOpenLink(deeplink, '_self');
} else {
this.openDeeplink(deeplink, universalLink);
}
});
this.transport.connect({ scopes, caipAccountIds }).then(resolve).catch(reject);
};
const deeplink = this.options.ui.factory.createDeeplink(connectionRequest);
const universalLink = this.options.ui.factory.createUniversalLink(connectionRequest);
if (this.options.mobile?.preferredOpenLink) {
this.options.mobile.preferredOpenLink(deeplink, '_self');
} else {
openDeeplink(this.options, deeplink, universalLink);
}
});
} else {
timeout = setTimeout(() => {
const deeplink = this.options.ui.factory.createDeeplink();
const universalLink = this.options.ui.factory.createUniversalLink();
if (this.options.mobile?.preferredOpenLink) {
this.options.mobile.preferredOpenLink(deeplink, '_self');
} else {
openDeeplink(this.options, deeplink, universalLink);
}
}, 250);
}

this.transport
.connect({ scopes, caipAccountIds })
.then(resolve)
.catch((err) => {
this.storage.removeTransport();
reject(err);
})
.finally(() => {
if (timeout) {
clearTimeout(timeout);
}
});
});
}

Expand All @@ -388,9 +395,18 @@ export class MultichainSDK extends MultichainCore {
const platformType = getPlatformType();
const isWeb = platformType === PlatformType.MetaMaskMobileWebview || platformType === PlatformType.DesktopWeb;
const { preferExtension = true, preferDesktop = false, headless: _headless = false } = ui;
const secure = isSecure();

if (this.__transport?.isConnected()) {
return this.handleConnection(this.__transport.connect({ scopes, caipAccountIds }));
if (this.__transport?.isConnected() && !secure) {
return this.handleConnection(
this.__transport.connect({ scopes, caipAccountIds }).then(() => {
if (this.__transport instanceof MWPTransport) {
return this.storage.setTransport(TransportType.MPW);
} else {
return this.storage.setTransport(TransportType.Browser);
}
}),
);
}

if (isWeb && hasExtension() && preferExtension) {
Expand All @@ -404,14 +420,8 @@ export class MultichainSDK extends MultichainCore {
await this.setupMWP();

// Determine preferred option for install modal
let isDesktopPreferred: boolean;
if (isWeb) {
isDesktopPreferred = hasExtension() ? preferDesktop : !preferExtension || preferDesktop;
} else {
isDesktopPreferred = preferDesktop;
}
const isDesktopPreferred = hasExtension() ? preferDesktop : !preferExtension || preferDesktop;

const secure = isSecure();
if (secure && !isDesktopPreferred) {
// Desktop is not preferred option, so we use deeplinks (mobile web)
return this.handleConnection(this.deeplinkConnect(scopes, caipAccountIds));
Expand Down Expand Up @@ -444,33 +454,11 @@ export class MultichainSDK extends MultichainCore {
}

async invokeMethod(request: InvokeMethodOptions): Promise<Json> {
const {
options: {
ui: { preferDesktop = false, headless: _headless = false },
},
sdkInfo,
transport,
} = this;
const { sdkInfo, transport } = this;

this.__provider ??= getMultichainClient({ transport });

const client = new RPCClient(this.transport, this.options.api, sdkInfo);
const secure = isSecure();

const shouldOpenDeeplink = secure && !preferDesktop;

// Call the client invoke method first
const invokePromise = client.invokeMethod(request);

// Schedule the deeplink to open 100ms after the invoke method is called
if (shouldOpenDeeplink) {
if (this.options.mobile?.preferredOpenLink) {
this.options.mobile.preferredOpenLink(METAMASK_DEEPLINK_BASE, '_self');
} else {
this.openDeeplink(METAMASK_DEEPLINK_BASE, METAMASK_CONNECT_BASE_URL);
}
}

return invokePromise as Promise<Json>;
const client = new RPCClient(this.transport, this.options, sdkInfo);
return client.invokeMethod(request) as Promise<Json>;
}
}
Loading
Loading