Skip to content
Open
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
4 changes: 3 additions & 1 deletion packages/connect-multichain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"test:ci": "yarn pretest:ci && vitest run --coverage --coverage.reporter=text --silent",
"test:unit": "vitest run",
"test:verbose": "vitest run --reporter=verbose",
"test:watch": "vitest watch"
"test:watch": "vitest watch",
"allow-scripts": ""
},
"dependencies": {
"@metamask/analytics": "workspace:^",
Expand All @@ -71,6 +72,7 @@
"@metamask/multichain-api-client": "^0.8.1",
"@metamask/multichain-ui": "workspace:^",
"@metamask/onboarding": "^1.0.1",
"@metamask/rpc-errors": "^7.0.3",
"@metamask/utils": "^11.8.1",
"@paulmillr/qr": "^0.2.1",
"bowser": "^2.11.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SessionRequest } from '@metamask/mobile-wallet-protocol-core';
import type { Transport, TransportRequest, TransportResponse } from '@metamask/multichain-api-client';
import type { SessionProperties, Transport, TransportRequest, TransportResponse } from '@metamask/multichain-api-client';
import type { CaipAccountId } from '@metamask/utils';

import type { MultichainCore } from '.';
Expand Down Expand Up @@ -93,6 +93,7 @@ export type ExtendedTransport = Omit<Transport, 'connect'> & {
connect: (props?: {
scopes: Scope[];
caipAccountIds: CaipAccountId[];
sessionProperties?: SessionProperties;
}) => Promise<void>;

sendEip1193Message: <TRequest extends TransportRequest, TResponse extends TransportResponse>(request: TRequest, options?: {
Expand Down
43 changes: 24 additions & 19 deletions packages/connect-multichain/src/multichain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { DappClient } from '@metamask/mobile-wallet-protocol-dapp-client';
import {
getMultichainClient,
SessionProperties,
type MultichainApiClient,
type SessionData,
} from '@metamask/multichain-api-client';
Expand Down Expand Up @@ -69,14 +70,17 @@ import { DefaultTransport } from './transports/default';
import { MWPTransport } from './transports/mwp';
import { keymanager } from './transports/mwp/KeyManager';
import { getDappId, openDeeplink, setupDappMetadata } from './utils';
import { MultichainApiClientWrapperTransport } from './transports/multichainApiClientWrapper';

export { getInfuraRpcUrls } from '../domain/multichain/api/infura';

// ENFORCE NAMESPACE THAT CAN BE DISABLED
const logger = createLogger('metamask-sdk:core');

export class MultichainSDK extends MultichainCore {
private __provider: MultichainApiClient<RPCAPI> | undefined = undefined;
private __provider: MultichainApiClient<RPCAPI>;

private __providerTransportWrapper: MultichainApiClientWrapperTransport;

private __transport: ExtendedTransport | undefined = undefined;

Expand All @@ -101,15 +105,6 @@ export class MultichainSDK extends MultichainCore {
}

get provider(): MultichainApiClient<RPCAPI> {
if (!this.__provider && this.__transport) {
this.__provider = getMultichainClient({ transport: this.__transport });
return this.__provider;
}

if (!this.__provider) {
throw new Error('Provider not initialized, establish connection first');
}

return this.__provider;
}

Expand Down Expand Up @@ -153,6 +148,9 @@ export class MultichainSDK extends MultichainCore {
};

super(allOptions);

this.__providerTransportWrapper = new MultichainApiClientWrapperTransport(this);
this.__provider = getMultichainClient({ transport: this.__providerTransportWrapper });
}

static async create(options: MultichainOptions): Promise<MultichainSDK> {
Expand Down Expand Up @@ -220,6 +218,7 @@ export class MultichainSDK extends MultichainCore {
if (hasExtensionInstalled) {
const apiTransport = new DefaultTransport();
this.__transport = apiTransport;
this.__providerTransportWrapper.setupNotifcationListener();
this.listener = apiTransport.onNotification(
this.onTransportNotification.bind(this),
);
Expand All @@ -231,6 +230,7 @@ export class MultichainSDK extends MultichainCore {
const apiTransport = new MWPTransport(dappClient, kvstore);
this.__dappClient = dappClient;
this.__transport = apiTransport;
this.__providerTransportWrapper.setupNotifcationListener();
this.listener = apiTransport.onNotification(
this.onTransportNotification.bind(this),
);
Expand Down Expand Up @@ -309,6 +309,7 @@ export class MultichainSDK extends MultichainCore {
this.__dappClient = dappClient;
const apiTransport = new MWPTransport(dappClient, kvstore);
this.__transport = apiTransport;
this.__providerTransportWrapper.setupNotifcationListener();
this.listener = this.transport.onNotification(
this.onTransportNotification.bind(this),
);
Expand Down Expand Up @@ -346,6 +347,7 @@ export class MultichainSDK extends MultichainCore {
desktopPreferred: boolean,
scopes: Scope[],
caipAccountIds: CaipAccountId[],
sessionProperties?: SessionProperties,
) {
// create the listener only once to avoid memory leaks
this.__beforeUnloadListener ??= this.createBeforeUnloadListener();
Expand Down Expand Up @@ -378,7 +380,7 @@ export class MultichainSDK extends MultichainCore {
);

this.transport
.connect({ scopes, caipAccountIds })
.connect({ scopes, caipAccountIds, sessionProperties })
.then(() => {
this.options.ui.factory.unload();
this.options.ui.factory.modal?.unmount();
Expand Down Expand Up @@ -421,12 +423,14 @@ export class MultichainSDK extends MultichainCore {
this.onTransportNotification.bind(this),
);
this.__transport = transport;
this.__providerTransportWrapper.setupNotifcationListener();
return transport;
}

private async deeplinkConnect(
scopes: Scope[],
caipAccountIds: CaipAccountId[],
sessionProperties?: SessionProperties,
) {
return new Promise<void>(async (resolve, reject) => {
this.dappClient.on('message', (payload: any) => {
Expand Down Expand Up @@ -486,7 +490,7 @@ export class MultichainSDK extends MultichainCore {
}

this.transport
.connect({ scopes, caipAccountIds })
.connect({ scopes, caipAccountIds, sessionProperties })
.then(resolve)
.catch((error) => {
this.storage.removeTransport();
Expand Down Expand Up @@ -515,6 +519,7 @@ export class MultichainSDK extends MultichainCore {
async connect(
scopes: Scope[],
caipAccountIds: CaipAccountId[],
sessionProperties?: SessionProperties,
): Promise<void> {
const { ui } = this.options;
const platformType = getPlatformType();
Expand All @@ -527,7 +532,7 @@ export class MultichainSDK extends MultichainCore {

if (this.__transport?.isConnected() && !secure) {
return this.handleConnection(
this.__transport.connect({ scopes, caipAccountIds }).then(() => {
this.__transport.connect({ scopes, caipAccountIds, sessionProperties }).then(() => {
if (this.__transport instanceof MWPTransport) {
return this.storage.setTransport(TransportType.MPW);
} else {
Expand All @@ -541,7 +546,7 @@ export class MultichainSDK extends MultichainCore {
if (platformType === PlatformType.MetaMaskMobileWebview) {
const defaultTransport = await this.setupDefaultTransport();
return this.handleConnection(
defaultTransport.connect({ scopes, caipAccountIds }),
defaultTransport.connect({ scopes, caipAccountIds, sessionProperties }),
);
}

Expand All @@ -550,7 +555,7 @@ export class MultichainSDK extends MultichainCore {
const defaultTransport = await this.setupDefaultTransport();
// Web transport has no initial payload
return this.handleConnection(
defaultTransport.connect({ scopes, caipAccountIds }),
defaultTransport.connect({ scopes, caipAccountIds, sessionProperties }),
);
}

Expand All @@ -565,13 +570,13 @@ export class MultichainSDK extends MultichainCore {
if (secure && !isDesktopPreferred) {
// Desktop is not preferred option, so we use deeplinks (mobile web)
return this.handleConnection(
this.deeplinkConnect(scopes, caipAccountIds),
this.deeplinkConnect(scopes, caipAccountIds, sessionProperties),
);
}

// Show install modal for RN, Web + Node
return this.handleConnection(
this.showInstallModal(isDesktopPreferred, scopes, caipAccountIds),
this.showInstallModal(isDesktopPreferred, scopes, caipAccountIds, sessionProperties),
);
}

Expand All @@ -593,17 +598,17 @@ export class MultichainSDK extends MultichainCore {
this.listener = undefined;
this.__beforeUnloadListener = undefined;
this.__transport = undefined;
this.__provider = undefined;
this.__providerTransportWrapper.clearNotificationCallbacks();
this.__dappClient = undefined;
}

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

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

const rpcClient = new RpcClient(options, sdkInfo);
const requestRouter = new RequestRouter(transport, rpcClient, options);
// need read only methods for solana
return requestRouter.invokeMethod(request);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type CreateSessionParams,
getDefaultTransport,
SessionProperties,
type Transport,
type TransportRequest,
type TransportResponse,
Expand Down Expand Up @@ -188,6 +189,7 @@ export class DefaultTransport implements ExtendedTransport {
async connect(options?: {
scopes: Scope[];
caipAccountIds: CaipAccountId[];
sessionProperties?: SessionProperties;
}): Promise<void> {
// Ensure message listener is set up before connecting
this.#setupMessageListener();
Expand Down Expand Up @@ -243,6 +245,7 @@ export class DefaultTransport implements ExtendedTransport {
);
const createSessionParams: CreateSessionParams<RPCAPI> = {
optionalScopes,
sessionProperties: options?.sessionProperties,
};
const response = await this.request(
{ method: 'wallet_createSession', params: createSessionParams },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { CreateSessionParams, Transport, TransportRequest, TransportResponse } from "@metamask/multichain-api-client";
import { providerErrors } from "@metamask/rpc-errors";
import { CaipAccountId } from "@metamask/utils";
import { InvokeMethodOptions, RPCAPI, Scope } from "src/domain";
import { MultichainSDK } from "src/multichain";

// uint32 (two's complement) max
// more conservative than Number.MAX_SAFE_INTEGER
const MAX = 4_294_967_295;
let idCounter = Math.floor(Math.random() * MAX);

const getUniqueId = (): number => {
idCounter = (idCounter + 1) % MAX;
return idCounter;
};

type TransportRequestWithId = TransportRequest & { id: number };

export class MultichainApiClientWrapperTransport implements Transport {
private requestId = getUniqueId();
private notificationCallbacks = new Set<(data: unknown) => void>();
constructor(private multichainSDK: MultichainSDK) {
}

isTransportDefined(): boolean {
try {
return Boolean(this.multichainSDK.transport)
} catch (error) {
return false;
}
}

clearNotificationCallbacks() {
this.notificationCallbacks.clear();
}

notifyCallbacks(data: unknown) {
this.notificationCallbacks.forEach((callback) => {
callback(data);
});
}

setupNotifcationListener() {
this.multichainSDK.transport.onNotification(this.notifyCallbacks.bind(this));
}

connect(): Promise<void> {
// noop
return Promise.resolve();
}

disconnect(): Promise<void> {
return Promise.resolve();
}

isConnected(): boolean {
return true
}

async request<ParamsType extends TransportRequest, ReturnType extends TransportResponse>(
params: ParamsType,
options: { timeout?: number } = {},
): Promise<ReturnType> {
const id = this.requestId++;
const requestPayload = {
id,
jsonrpc: '2.0',
...params,
};

if (requestPayload.method === 'wallet_createSession') {
return this.#walletCreateSession(requestPayload) as Promise<ReturnType>;
} else if (requestPayload.method === 'wallet_getSession') {
return this.#walletGetSession(requestPayload) as Promise<ReturnType>;
} else if (requestPayload.method === 'wallet_revokeSession') {
return this.#walletRevokeSession(requestPayload) as Promise<ReturnType>;
} else if (requestPayload.method === 'wallet_invokeMethod') {
return this.#walletInvokeMethod(requestPayload) as Promise<ReturnType>;
}

throw new Error(`Unknown method: ${requestPayload.method}`);
}

onNotification(callback: (data: unknown) => void) {
if (!this.isTransportDefined()) {
this.notificationCallbacks.add(callback);
return () => {
this.notificationCallbacks.delete(callback);
};
}

return this.multichainSDK.transport.onNotification(callback);
}

async #walletCreateSession(request: TransportRequestWithId) {
const createSessionParams = request.params as CreateSessionParams<RPCAPI>;
const scopes = Object.keys({...createSessionParams.optionalScopes, ...createSessionParams.requiredScopes}) as Scope[]
const scopeAccounts: CaipAccountId[] = [];

scopes.forEach((scope) => {
const requiredScope = createSessionParams.requiredScopes?.[scope];
const optionalScope = createSessionParams.optionalScopes?.[scope];
if (requiredScope) {
scopeAccounts.push(...(requiredScope.accounts ?? []));
}

if (optionalScope) {
scopeAccounts.push(...(optionalScope.accounts ?? []));
}
});
const accounts = [...new Set(scopeAccounts)];


await this.multichainSDK.connect(scopes, accounts, createSessionParams.sessionProperties)
return this.multichainSDK.transport.request({ method: 'wallet_getSession' });
}

async #walletGetSession(request: TransportRequestWithId) {
if (!this.isTransportDefined()) {
return {
jsonrpc: '2.0',
id: request.id,
result: {
"sessionScopes": {}
}
}
}
return this.multichainSDK.transport.request({ method: 'wallet_getSession' });
}

async #walletRevokeSession(request: TransportRequestWithId) {
if (!this.isTransportDefined()) {
return { jsonrpc: '2.0', id: request.id, result: true };
}

try {
this.multichainSDK.disconnect()
return { jsonrpc: '2.0', id: request.id, result: true }
} catch (error) {
return { jsonrpc: '2.0', id: request.id, result: false }
}
}

async #walletInvokeMethod(request: TransportRequestWithId) {
if (!this.isTransportDefined()) {
return { error: providerErrors.unauthorized() }
}
return this.multichainSDK.invokeMethod(request.params as InvokeMethodOptions)
}
}
Loading