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
45 changes: 45 additions & 0 deletions .pnpmfile.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,56 @@ function stripWalletConnectDependencies(pkg, context) {
}
}

// --- License compliance: strip the MetaMask SDK -----------------------------
// @metamask/sdk ships a proprietary ConsenSys licence, not an open-source one:
// "Copyright ConsenSys Software Inc. 2022. All rights reserved", granting only a
// non-exclusive, non-transferable licence for Non-Commercial Use -- and clause 2
// requires any Resulting Program to carry that same Non-Commercial restriction
// forward.
//
// The OpenZeppelin adapters are AGPL-3.0, which forbids conveying the work under
// added restrictions. A non-commercial-only restriction is exactly such a
// restriction, so the two licences cannot both be satisfied. The conflict is
// structural: it does not depend on a monthly-active-user count, and there is no
// clean version to pin (no published version declares a `license` field at all).
//
// The same 2715-byte licence file ships in @metamask/sdk,
// @metamask/sdk-communication-layer and @metamask/sdk-install-modal-web.
//
// Scoped to those three package names on purpose. Most of @metamask/* is MIT or
// ISC (utils, providers, json-rpc-engine, rpc-errors, superstruct, ...) and is
// legitimately needed; a scope-wide strip would break far more than it fixes.
//
// @wagmi/connectors declares @metamask/sdk as a hard dependency, not an optional
// peer, so it installs whether or not a metaMask() connector is registered.
const METAMASK_STRIP = {
'@wagmi/connectors': ['@metamask/sdk'],
};
const METAMASK_DEP_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependencies'];

function stripMetaMaskDependencies(pkg, context) {
const deps = METAMASK_STRIP[pkg.name];
if (!deps) {
return;
}

for (const field of METAMASK_DEP_FIELDS) {
for (const dep of deps) {
if (pkg[field] && dep in pkg[field]) {
delete pkg[field][dep];
context.log(`[license] stripped ${dep} from ${pkg.name}@${pkg.version} (MetaMask SDK)`);
}
}
}
}

function readPackage(pkg, context) {
stripWalletConnectDependencies(pkg, context);

stripTrezorDependencies(pkg, context);

stripMetaMaskDependencies(pkg, context);

if (isAnyLocalFamilyEnabled()) {
const workspaceRoot = __dirname;
const projectConfig = readProjectConfig(workspaceRoot);
Expand Down
26 changes: 13 additions & 13 deletions apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@
"@creit.tech/stellar-wallets-kit": "^1.9.5",
"@hookform/resolvers": "^4.1.3",
"@icons-pack/react-simple-icons": "^12.9.0",
"@openzeppelin/adapter-evm": "^4.0.0",
"@openzeppelin/adapter-midnight": "^4.0.0",
"@openzeppelin/adapter-polkadot": "^3.0.0",
"@openzeppelin/adapter-solana": "^4.0.0",
"@openzeppelin/adapter-stellar": "^4.0.0",
"@openzeppelin/ui-components": "^3.8.2",
"@openzeppelin/ui-react": "^3.3.1",
"@openzeppelin/ui-renderer": "^3.4.1",
"@openzeppelin/ui-storage": "^1.2.4",
"@openzeppelin/ui-styles": "^1.1.0",
"@openzeppelin/ui-types": "^3.5.1",
"@openzeppelin/ui-utils": "^4.0.0",
"@openzeppelin/adapter-evm": "^5.0.0",
"@openzeppelin/adapter-midnight": "^4.0.1",
"@openzeppelin/adapter-polkadot": "^5.0.0",
"@openzeppelin/adapter-solana": "^4.0.1",
"@openzeppelin/adapter-stellar": "^4.0.1",
"@openzeppelin/ui-components": "^3.8.3",
"@openzeppelin/ui-react": "^3.3.2",
"@openzeppelin/ui-renderer": "^3.4.2",
"@openzeppelin/ui-storage": "^1.2.5",
"@openzeppelin/ui-styles": "^1.1.1",
"@openzeppelin/ui-types": "^3.5.2",
"@openzeppelin/ui-utils": "^4.0.1",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
Expand Down Expand Up @@ -88,7 +88,7 @@
"@midnight-ntwrk/midnight-js-types": "2.0.2",
"@midnight-ntwrk/midnight-js-utils": "2.0.2",
"@midnight-ntwrk/zswap": "4.0.0",
"@openzeppelin/adapters-vite": "^11.0.0",
"@openzeppelin/adapters-vite": "^12.0.0",
"@tailwindcss/vite": "^4.2.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, expect, it } from 'vitest';

import { getNetworksByEcosystem } from '../../core/ecosystemManager';
import { AppExportSystem } from '../AppExportSystem';
import { createMinimalContractSchema, createMinimalFormConfig } from '../utils/testConfig';
import { extractFilesFromZip } from '../utils/zipInspector';

/**
* `@metamask/sdk` is not open source: it ships a proprietary ConsenSys licence
* granting only a licence for Non-Commercial Use, and requiring any derivative to
* carry that same restriction forward. The OpenZeppelin adapters are AGPL-3.0,
* which forbids conveying the work under added restrictions, so the two cannot both
* be satisfied.
*
* `@wagmi/connectors` declares it as a hard dependency rather than an optional peer,
* so it installs whether or not a `metaMask()` connector is registered. An exported
* app therefore needs both halves:
*
* - `.pnpmfile.cjs` to keep it out of the install tree, and
* - a `vite.config.ts` alias to a stub, because `@wagmi/connectors` re-exports an
* unreachable `metaMask` module containing `await import('@metamask/sdk')` and
* Rollup resolves dynamic imports at build time. Without the alias the generated
* app fails to build with "Rollup failed to resolve import".
*
* Exercises the EVM path: the Stellar export cannot run under vitest because
* @stellar/freighter-api is CJS and breaks named-export interop there.
*/
describe('exported app excludes the proprietary MetaMask SDK', () => {
it('strips it at install time and aliases the bundler to a stub', async () => {
const networks = await getNetworksByEcosystem('evm');
const networkConfig = networks.find((n) => n.id === 'ethereum-mainnet') ?? networks[0];
expect(networkConfig).toBeDefined();

const exportSystem = new AppExportSystem();
const result = await exportSystem.exportApp(
createMinimalFormConfig('transfer', 'evm'),
createMinimalContractSchema('transfer', 'evm'),
networkConfig,
'transfer',
{ projectName: 'metamask-exclusion-verify', env: 'production' }
);

const files = await extractFilesFromZip(result.data);

// 1. The install-tree half.
const hook = files['.pnpmfile.cjs'];
expect(hook).toBeDefined();
expect(hook).toContain("'@wagmi/connectors': ['@metamask/sdk']");
expect(hook).toContain('function stripMetaMaskDependencies(pkg, context)');
expect(hook).toContain('stripMetaMaskDependencies(pkg, context)');

// 2. The bundler half, or the generated app cannot build.
expect(files['src/shims/metamask-removed.ts']).toBeDefined();
expect(files['vite.config.ts']).toContain("'@metamask/sdk'");
expect(files['vite.config.ts']).toContain('./src/shims/metamask-removed.ts');

// 3. The SDK must never be a declared dependency of an exported app.
const packageJson = JSON.parse(files['package.json']);
const allDeps = Object.keys({
...((packageJson.dependencies ?? {}) as Record<string, string>),
...((packageJson.devDependencies ?? {}) as Record<string, string>),
});

expect(allDeps).not.toContain('@metamask/sdk');
expect(allDeps).not.toContain('@metamask/sdk-communication-layer');
expect(allDeps).not.toContain('@metamask/sdk-install-modal-web');
});

it('bans the three SDK names exactly, not the whole @metamask scope', async () => {
// Most of @metamask/* is MIT or ISC and is legitimately required transitively
// (utils, providers, json-rpc-engine, rpc-errors, superstruct, sdk-analytics).
// A scope-wide strip would break far more than it fixes, so the hook must name
// the SDK explicitly and must not reference the bare scope.
const networks = await getNetworksByEcosystem('evm');
const networkConfig = networks.find((n) => n.id === 'ethereum-mainnet') ?? networks[0];

const exportSystem = new AppExportSystem();
const result = await exportSystem.exportApp(
createMinimalFormConfig('transfer', 'evm'),
createMinimalContractSchema('transfer', 'evm'),
networkConfig,
'transfer',
{ projectName: 'metamask-scope-verify', env: 'production' }
);

const hook = (await extractFilesFromZip(result.data))['.pnpmfile.cjs'];

// Assert the mechanism, not the prose: the strip list names the SDK exactly,
// and nothing matches the scope by prefix. (The hook's comments legitimately
// mention `@metamask/*` when explaining why the scope is not banned.)
expect(hook).toContain("'@wagmi/connectors': ['@metamask/sdk']");
expect(hook).not.toContain("startsWith('@metamask");
expect(hook).not.toMatch(/METAMASK_STRIP\s*=\s*\{[^}]*@metamask\/(?!sdk')/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -341,12 +341,12 @@ export default function GeneratedForm({ adapter, isWalletConnected }: GeneratedF
exports[`Export Snapshot Tests > evm Export Snapshots > should match snapshot for package.json structure > package-json-evm 1`] = `
{
"dependencies": {
"@openzeppelin/adapter-evm": "^4.0.0",
"@openzeppelin/ui-components": "^3.8.2",
"@openzeppelin/ui-react": "^3.3.1",
"@openzeppelin/ui-renderer": "^3.4.1",
"@openzeppelin/ui-types": "^3.5.1",
"@openzeppelin/ui-utils": "^4.0.0",
"@openzeppelin/adapter-evm": "^5.0.0",
"@openzeppelin/ui-components": "^3.8.3",
"@openzeppelin/ui-react": "^3.3.2",
"@openzeppelin/ui-renderer": "^3.4.2",
"@openzeppelin/ui-types": "^3.5.2",
"@openzeppelin/ui-utils": "^4.0.1",
"@tanstack/react-query": "^5.0.0",
"@wagmi/core": "^2.20.3",
"react": "^19.2.1",
Expand Down Expand Up @@ -708,12 +708,12 @@ export default function GeneratedForm({ adapter, isWalletConnected }: GeneratedF
exports[`Export Snapshot Tests > polkadot Export Snapshots > should match snapshot for package.json structure > package-json-polkadot 1`] = `
{
"dependencies": {
"@openzeppelin/adapter-polkadot": "^3.0.0",
"@openzeppelin/ui-components": "^3.8.2",
"@openzeppelin/ui-react": "^3.3.1",
"@openzeppelin/ui-renderer": "^3.4.1",
"@openzeppelin/ui-types": "^3.5.1",
"@openzeppelin/ui-utils": "^4.0.0",
"@openzeppelin/adapter-polkadot": "^5.0.0",
"@openzeppelin/ui-components": "^3.8.3",
"@openzeppelin/ui-react": "^3.3.2",
"@openzeppelin/ui-renderer": "^3.4.2",
"@openzeppelin/ui-types": "^3.5.2",
"@openzeppelin/ui-utils": "^4.0.1",
"@tanstack/react-query": "^5.0.0",
"@wagmi/core": "^2.20.3",
"react": "^19.2.1",
Expand Down
13 changes: 13 additions & 0 deletions apps/builder/src/export/generators/ViteConfigGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ export function generateViteConfig(options: ViteConfigGeneratorOptions): string
].join('\n')
: '';

// The MetaMask SDK is stripped for licence reasons (proprietary, Non-Commercial
// Use only) and needs the same treatment: @wagmi/connectors re-exports an
// unreachable metaMask module that dynamically imports it.
const metaMaskStubAlias = usesWalletInterop
? [
" '@metamask/sdk': path.resolve(",
' __dirname,',
" './src/shims/metamask-removed.ts'",
' ),',
].join('\n')
: '';

const optimizeDepsInclude = buildOptimizeDepsInclude(
usesWalletInterop,
viteConfig?.optimizeDeps?.include
Expand All @@ -124,6 +136,7 @@ ${plugins.join('\n')}
events: 'events/',
${eventemitter3Alias}
${walletConnectStubAlias}
${metaMaskStubAlias}
},
${dedupeConfig}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,47 @@ function stripWalletConnectDependencies(pkg, context) {
}
}

// --- License compliance: strip the MetaMask SDK -----------------------------
// @metamask/sdk ships a proprietary ConsenSys licence, not an open-source one:
// "Copyright ConsenSys Software Inc. 2022. All rights reserved", granting only a
// licence for Non-Commercial Use, and requiring any derivative to carry that same
// restriction forward. The OpenZeppelin adapters are AGPL-3.0, which forbids
// conveying the work under added restrictions, so the two cannot both be
// satisfied. No published version declares a `license` field at all.
//
// @wagmi/connectors declares it as a hard dependency, not an optional peer, so it
// installs whether or not a metaMask() connector is registered.
//
// Scoped to this one name on purpose: most of @metamask/* is MIT or ISC and is
// legitimately needed, so a scope-wide strip would break far more than it fixes.
const METAMASK_STRIP = {
'@wagmi/connectors': ['@metamask/sdk'],
};
const METAMASK_DEP_FIELDS = ['dependencies', 'optionalDependencies', 'peerDependencies'];

function stripMetaMaskDependencies(pkg, context) {
const deps = METAMASK_STRIP[pkg.name];
if (!deps) {
return;
}

for (const field of METAMASK_DEP_FIELDS) {
for (const dep of deps) {
if (pkg[field] && dep in pkg[field]) {
delete pkg[field][dep];
context.log(`[license] stripped ${dep} from ${pkg.name}@${pkg.version} (MetaMask SDK)`);
}
}
}
}

function readPackage(pkg, context) {
stripWalletConnectDependencies(pkg, context);

stripTrezorDependencies(pkg, context);

stripMetaMaskDependencies(pkg, context);

return pkg;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
"check-deps": "npm outdated"
},
"dependencies": {
"@openzeppelin/ui-components": "^3.8.0",
"@openzeppelin/ui-react": "^3.3.0",
"@openzeppelin/ui-renderer": "^3.4.0",
"@openzeppelin/ui-types": "^3.3.0",
"@openzeppelin/ui-utils": "^3.3.0",
"@openzeppelin/ui-components": "^3.8.3",
"@openzeppelin/ui-react": "^3.3.2",
"@openzeppelin/ui-renderer": "^3.4.2",
"@openzeppelin/ui-types": "^3.5.2",
"@openzeppelin/ui-utils": "^4.0.1",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-hook-form": "^7.60.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Stub for the MetaMask SDK that `@wagmi/connectors` dynamically imports.
*
* `@metamask/sdk` is not open source: it ships a proprietary ConsenSys licence
* granting only a non-exclusive, non-transferable licence for Non-Commercial Use,
* whose clause 2 requires any derivative to carry that same restriction forward.
* That cannot be reconciled with AGPL-3.0, which forbids conveying the work under
* added restrictions, so `.pnpmfile.cjs` strips it from the install tree.
*
* `@wagmi/connectors` still re-exports its `metaMask` connector, whose module
* contains `await import('@metamask/sdk')`. Rollup resolves dynamic imports at
* build time even when the call site is unreachable, so without this alias the
* production build fails with "failed to resolve import".
*
* Nothing registers that connector, so this module is never evaluated. It throws
* rather than returning a fake SDK so that any future accidental use is loud.
*
* The connector unwraps the import as `const { default: SDK } = await import(...)`
* and then calls `new SDK({...})`, so the default export must be constructible.
*/

class MetaMaskSdkRemoved {
constructor() {
throw new Error(
'The MetaMask SDK was removed from this application for licence reasons ' +
'(proprietary, Non-Commercial Use only). The MetaMask browser extension is ' +
'still available through the injected connector and EIP-6963 discovery; ' +
'MetaMask mobile deep-link / QR pairing is not.'
);
}
}

export default MetaMaskSdkRemoved;
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

function walletConnectRemoved(): never {
throw new Error(
'WalletConnect support was removed from this application. Use an injected wallet, MetaMask or Safe.'
'WalletConnect support was removed from this application. Use an injected wallet (the MetaMask extension included) or Safe.'
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export default defineConfig(({ mode }) => ({
__dirname,
'./src/shims/walletconnect-removed.ts'
),
// The MetaMask SDK is stripped from the install tree for licence reasons
// (proprietary, Non-Commercial Use only). @wagmi/connectors still ships an
// unreachable metaMask module that dynamically imports it, so point it at a
// stub. See src/shims/metamask-removed.ts.
'@metamask/sdk': path.resolve(__dirname, './src/shims/metamask-removed.ts'),
},
},
define: {
Expand Down
Loading
Loading