Problem
packages/swap-widget/package.json marks seven peers optional: true in peerDependenciesMeta. The intent is graceful degradation — a consumer who doesn't install the Solana or Bitcoin packages simply doesn't get those wallets.
That isn't what happens. Every one of the seven is reached by a static value import from the single src/index.ts entrypoint, and tsup emits one flat ESM bundle with all of them marked external. The specifiers sit at the top of dist/index.js, so the consumer's bundler has to resolve them whether or not they ever touch Solana. An EVM-only integrator following the documented install list gets a module resolution failure, not a widget with Solana quietly switched off.
Audit
| optional peer |
static value import |
reachable from entrypoint via |
@reown/appkit |
WalletProvider.tsx:1, SwapWidget.tsx:3 |
direct |
@reown/appkit-adapter-wagmi |
config/appkit.ts (WagmiAdapter) |
SwapWidget → WalletProvider → appkit |
@reown/appkit-adapter-bitcoin |
config/appkit.ts (BitcoinAdapter) |
same |
@reown/appkit-adapter-solana |
config/appkit.ts (SolanaAdapter), useBalances.ts:1, useSolanaSigning.ts:3 |
same + TokenSelectModal |
@solana/web3.js |
useBalances.ts:3, utils/addressValidation.ts:2 |
SwapWidget.tsx:23 → validateAddress |
@solana/wallet-adapter-phantom |
config/appkit.ts |
SwapWidget → WalletProvider → appkit |
@solana/wallet-adapter-solflare |
config/appkit.ts |
same |
ESM imports are hoisted and eagerly resolved, so it doesn't help that new PhantomWalletAdapter() only runs inside initializeAppKit() — the import is resolved at module load regardless of whether that function is ever called.
Note that someone already reached for the right technique: useSwapExecution.ts:84 and :156 do await import('@solana/web3.js'). It buys nothing today, because utils/addressValidation.ts:2 value-imports PublicKey from the same package and SwapWidget.tsx:23 pulls that in unconditionally. The lazy path is defeated by a static one two files over.
Options
- Separate entrypoint — move the AppKit/Solana surface behind
@shapeshiftoss/swap-widget/appkit (or similar) that consumers opt into via exports. Cleanest signal, but changes the package's public shape and the integration docs.
- Dynamic imports throughout — convert the adapter construction and the
PublicKey/Connection uses to await import(...), guarding the optional-peer paths. Keeps one entrypoint; needs the addressValidation/useBalances call sites to tolerate async.
Either way the install snippets in packages/swap-widget/README.md and packages/public-api/docs/swap-widget-sdk.md need to say which packages are genuinely required for which chain families.
Context
Surfaced during review of #12601, which swapped @solana/wallet-adapter-wallets for the two adapters actually used. That PR doesn't change this situation — one non-functional optional marking became two, contract identical — and its description calls the underlying problem out as a known follow-up. CodeRabbit flagged the two adapters on that PR and was told the same; marking just those two required would have fixed nothing while the other five static imports remained.
Related: @shapeshiftoss/caip statically bundles the generated CoinGecko/CoinCap maps for every chain (~807 KB gzipped, Base alone ~179 KB), which is a similar all-or-nothing weight problem for chain-scoped integrators.
Problem
packages/swap-widget/package.jsonmarks seven peersoptional: trueinpeerDependenciesMeta. The intent is graceful degradation — a consumer who doesn't install the Solana or Bitcoin packages simply doesn't get those wallets.That isn't what happens. Every one of the seven is reached by a static value import from the single
src/index.tsentrypoint, and tsup emits one flat ESM bundle with all of them marked external. The specifiers sit at the top ofdist/index.js, so the consumer's bundler has to resolve them whether or not they ever touch Solana. An EVM-only integrator following the documented install list gets a module resolution failure, not a widget with Solana quietly switched off.Audit
@reown/appkitWalletProvider.tsx:1,SwapWidget.tsx:3@reown/appkit-adapter-wagmiconfig/appkit.ts(WagmiAdapter)@reown/appkit-adapter-bitcoinconfig/appkit.ts(BitcoinAdapter)@reown/appkit-adapter-solanaconfig/appkit.ts(SolanaAdapter),useBalances.ts:1,useSolanaSigning.ts:3@solana/web3.jsuseBalances.ts:3,utils/addressValidation.ts:2SwapWidget.tsx:23→validateAddress@solana/wallet-adapter-phantomconfig/appkit.ts@solana/wallet-adapter-solflareconfig/appkit.tsESM imports are hoisted and eagerly resolved, so it doesn't help that
new PhantomWalletAdapter()only runs insideinitializeAppKit()— the import is resolved at module load regardless of whether that function is ever called.Note that someone already reached for the right technique:
useSwapExecution.ts:84and:156doawait import('@solana/web3.js'). It buys nothing today, becauseutils/addressValidation.ts:2value-importsPublicKeyfrom the same package andSwapWidget.tsx:23pulls that in unconditionally. The lazy path is defeated by a static one two files over.Options
@shapeshiftoss/swap-widget/appkit(or similar) that consumers opt into viaexports. Cleanest signal, but changes the package's public shape and the integration docs.PublicKey/Connectionuses toawait import(...), guarding the optional-peer paths. Keeps one entrypoint; needs theaddressValidation/useBalancescall sites to tolerate async.Either way the install snippets in
packages/swap-widget/README.mdandpackages/public-api/docs/swap-widget-sdk.mdneed to say which packages are genuinely required for which chain families.Context
Surfaced during review of #12601, which swapped
@solana/wallet-adapter-walletsfor the two adapters actually used. That PR doesn't change this situation — one non-functional optional marking became two, contract identical — and its description calls the underlying problem out as a known follow-up. CodeRabbit flagged the two adapters on that PR and was told the same; marking just those tworequiredwould have fixed nothing while the other five static imports remained.Related:
@shapeshiftoss/caipstatically bundles the generated CoinGecko/CoinCap maps for every chain (~807 KB gzipped, Base alone ~179 KB), which is a similar all-or-nothing weight problem for chain-scoped integrators.