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
15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"dist/"
],
"scripts": {
"dev": "vite --host 0.0.0.0 --port 8081",
"dev": "vite --host 0.0.0.0 --port 8081 --force",
"build": "tsc -b && vite build && tsc --project tsconfig.test.json --outDir dist",
"format": "biome format",
"format:fix": "biome format --write",
Expand Down Expand Up @@ -49,7 +49,16 @@
"last 1 safari version"
]
},
"resolutions": {
"@metamask/connect-multichain": "file:../connect-monorepo/packages/connect-multichain",
"@metamask/multichain-ui": "file:../connect-monorepo/packages/multichain-ui",
"@metamask/analytics": "file:../connect-monorepo/packages/analytics"
},
"dependencies": {
"@metamask/connect-multichain": "file:../connect-monorepo/packages/connect-multichain",
Copy link

Choose a reason for hiding this comment

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

Local file dependencies break builds in non-local environments

High Severity

The resolutions and dependencies contain local file paths like file:../connect-monorepo/packages/connect-multichain that reference a sibling directory. These paths will fail in CI/CD pipelines, other developers' machines, and production builds since the expected local directory structure won't exist. This appears to be development-time configuration that was accidentally included in the PR.

Fix in Cursor Fix in Web

"@metamask/multichain-api-client": "^0.10.0",
"@metamask/rpc-errors": "^7.0.3",
"@metamask/solana-wallet-standard": "^0.6.0",
"@solana/spl-token": "^0.4.13",
"@solana/wallet-adapter-base": "^0.9.23",
"@solana/wallet-adapter-react": "^0.15.35",
Expand All @@ -58,6 +67,7 @@
"@solana/wallet-standard-chains": "^1.1.1",
"@solana/wallet-standard-util": "^1.1.2",
"@solana/web3.js": "^1.95.5",
"bowser": "^2.13.1",
"buffer": "^6.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down Expand Up @@ -106,7 +116,8 @@
"vite>esbuild": false,
"jsdom>ws>bufferutil": false,
"jsdom>ws>utf-8-validate": false,
"@solana/spl-token>@solana/buffer-layout-utils>bigint-buffer": false
"@solana/spl-token>@solana/buffer-layout-utils>bigint-buffer": false,
"@metamask/connect-multichain>@metamask/mobile-wallet-protocol-core>centrifuge>protobufjs": false
}
}
}
25 changes: 22 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import { type FC, useMemo } from 'react';
import { type FC, useEffect, useMemo, useRef } from 'react';

import '@solana/wallet-adapter-react-ui/styles.css';
import { TestPage } from './pages/TestPage';

import { registerSolanaWalletStandard } from '@metamask/solana-wallet-standard';
import { CoinbaseWalletAdapter, PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets';
import { useSDK } from './SDKProvider';
import { EndpointProvider, useEndpoint } from './context/EndpointProvider';
import { TestPage } from './pages/TestPage';

const AppContent: FC = () => {
const { endpoint } = useEndpoint();
Expand All @@ -16,6 +17,24 @@ const AppContent: FC = () => {
[endpoint],
);

const { getProvider } = useSDK();
const registered = useRef(false);
useEffect(() => {
if (registered.current) {
return;
}
registered.current = true;
(async () => {
// TODO: fix this
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for the SDK to be initialized

const provider = await getProvider();
if (provider) {
registerSolanaWalletStandard({ client: provider });
}
})();
Copy link

Choose a reason for hiding this comment

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

Unhandled promise rejection when getProvider throws

Medium Severity

The async IIFE in the useEffect calls getProvider() without any error handling. Unlike other SDK methods (connect, disconnect, invokeMethod) which catch errors and call setError(), getProvider throws errors directly. If SDK initialization fails or createMetamaskConnect rejects, this results in an unhandled promise rejection, causing silent failure of wallet registration.

Additional Locations (1)

Fix in Cursor Fix in Web

}, [getProvider]);

return (
<ConnectionProvider endpoint={endpoint} config={{ commitment: 'confirmed' }}>
<WalletProvider wallets={wallets} autoConnect={true}>
Expand Down
135 changes: 135 additions & 0 deletions src/SDKProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/* eslint-disable */

import {
type InvokeMethodOptions,
type MultichainCore,
type SDKState,
type Scope,
type SessionData,
createMultichainClient,
getInfuraRpcUrls,
} from '@metamask/connect-multichain';
import type { MultichainApiClient } from '@metamask/multichain-api-client';
import type { CaipAccountId } from '@metamask/utils';
import type React from 'react';
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';

const METAMASK_PROD_CHROME_ID = 'nkbihfbeogaeaoehlefnkodbefgpgknn';

const SDKContext = createContext<
| {
session: SessionData | undefined;
state: SDKState;
error: Error | null;
connect: (scopes: Scope[], caipAccountIds: CaipAccountId[]) => Promise<void>;
disconnect: () => Promise<void>;
invokeMethod: (options: InvokeMethodOptions) => Promise<any>;
getProvider: () => Promise<MultichainApiClient>;
}
| undefined
>(undefined);

export const SDKProvider = ({ children }: { children: React.ReactNode }) => {
const [state, setState] = useState<SDKState>('pending');
const [session, setSession] = useState<SessionData | undefined>(undefined);
const [error, setError] = useState<Error | null>(null);

const sdkRef = useRef<Promise<MultichainCore>>(undefined);

useEffect(() => {
if (!sdkRef.current) {
sdkRef.current = createMultichainClient({
dapp: {
name: 'playground',
url: 'https://playground.metamask.io',
},
api: {
supportedNetworks: getInfuraRpcUrls(process.env.INFURA_API_KEY || ''),
Copy link

Choose a reason for hiding this comment

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

Environment variable inaccessible in Vite browser context

Medium Severity

The code uses process.env.INFURA_API_KEY which will always be undefined in Vite's browser runtime. The codebase already uses import.meta.env.VITE_* for environment variables (e.g., in src/config.ts), and there's no define configuration in vite.config.ts to substitute process.env values. The fallback empty string will always be used, causing getInfuraRpcUrls('') to receive an invalid API key.

Fix in Cursor Fix in Web

},
transport: {
extensionId: METAMASK_PROD_CHROME_ID,
onNotification: (notification: unknown) => {
const payload = notification as Record<string, unknown>;
if (
payload.method === 'wallet_sessionChanged' ||
payload.method === 'wallet_createSession' ||
payload.method === 'wallet_getSession'
) {
setSession(payload.params as SessionData);
} else if (payload.method === 'stateChanged') {
setState(payload.params as SDKState);
}
},
},
});
}
}, []);

const disconnect = useCallback(async () => {
try {
if (!sdkRef.current) {
throw new Error('SDK not initialized');
}
const sdkInstance = await sdkRef.current;
return sdkInstance.disconnect();
} catch (error) {
setError(error as Error);
}
}, []);

const connect = useCallback(async (scopes: Scope[], caipAccountIds: CaipAccountId[]) => {
try {
if (!sdkRef.current) {
throw new Error('SDK not initialized');
}
const sdkInstance = await sdkRef.current;
await sdkInstance.connect(scopes, caipAccountIds);
} catch (error) {
setError(error as Error);
}
}, []);

const invokeMethod = useCallback(async (options: InvokeMethodOptions) => {
try {
if (!sdkRef.current) {
throw new Error('SDK not initialized');
}
const sdkInstance = await sdkRef.current;
return sdkInstance.invokeMethod(options);
} catch (error) {
setError(error as Error);
}
}, []);

const getProvider = useCallback(async () => {
if (!sdkRef.current) {
throw new Error('SDK not initialized');
}
const sdkInstance = await sdkRef.current;
return sdkInstance.provider as MultichainApiClient;
}, []);
Copy link

Choose a reason for hiding this comment

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

Inconsistent error handling in getProvider callback

Medium Severity

The getProvider function throws errors directly without catching them or calling setError, unlike disconnect, connect, and invokeMethod which all wrap their logic in try-catch blocks and store errors in state. This inconsistency means errors from getProvider won't appear in the context's error state and will propagate as unhandled rejections.

Fix in Cursor Fix in Web


return (
<SDKContext.Provider
value={{
session,
state,
error,
connect,
disconnect,
invokeMethod,
getProvider,
}}
>
{children}
</SDKContext.Provider>
);
};

export const useSDK = () => {
const context = useContext(SDKContext);
if (context === undefined) {
throw new Error('useSDK must be used within a SDKProvider');
}
return context;
};
5 changes: 4 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import { App } from './App.tsx';
import { SDKProvider } from './SDKProvider.tsx';

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<SDKProvider>
<App />
</SDKProvider>
</StrictMode>,
);
13 changes: 13 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ export default defineConfig({
},
}),
],
optimizeDeps: {
include: ['bowser'],
esbuildOptions: {
// Force bowser to use the CommonJS entry point which has proper exports
mainFields: ['main', 'module'],
},
},
resolve: {
alias: {
// Force all bowser imports to use the root-level bowser package (v2.13.1)
bowser: 'bowser/es5.js',
},
},
test: {
// 👋 add the line below to add jsdom to vite
environment: 'jsdom',
Expand Down
Loading
Loading