Migration plan — adopt @swarm-ai-labs/turnkey-extensions in ai-exchange-web
Goal: make ai-exchange-web depend on only @swarm-ai-labs/turnkey-extensions for crypto — delete the duplicated integration code and drop every chain SDK from the app's package.json, importing it all from the package instead.
This is a mechanical, domain-by-domain migration. Each step compiles and is independently verifiable; nothing is behind a feature flag, so revert = git revert of that step's commit.
0. Decide the dependency boundary (do this first)
"Remove all packages except ours" forces one design decision: who owns the chain SDKs at install time. The package currently declares them as optional peerDependencies (the app would still have to install them). To let the app drop them, reclassify them in the package:
| Package dep | Now | Change to | Why |
|---|---|---|---|
@solana/web3.js, @near-js/*, near-api-js, tronweb, @ton/*, @mysten/sui, @nktkas/hyperliquid, @polymarket/clob-client-v2, @turnkey/solana, @turnkey/viem | optional peer | dependencies | App no longer imports them directly → must install transitively via us |
@scure/*, @noble/curves, borsh | dependency | dependency (unchanged) | small, already bundled by us |
viem | dependency | peerDependency | App uses viem widely; both sides must share one instance (types + instanceof) |
react, react-dom, @turnkey/react-wallet-kit | optional peer | peer (unchanged) | App uses these directly (TurnkeyProvider, useTurnkey) |
@aurora-is-near/intents-swap-widget* | optional peer | peer (unchanged) | Widget JSX stays in the app |
Trade-off: bundling the chain SDKs as
dependenciesmeans the app can't pin their versions independently and may double-install if it ever re-adds one. That is the explicit cost of "one package."viemis the one exception kept as a peer because a duplicated viem instance breaks type identity.
After this change the app's removable set (step 6) installs transitively through us.
1. Link the package
Local dev (mirrors the existing pieui workflow in the app's scripts):
# in turnkey-extensions
bun run build && bun link
# in ai-exchange-web
bun link @swarm-ai-labs/turnkey-extensions && bun installFor CI/prod, pin the tarball attached to the GitHub Release — this package is not published to any registry:
"@swarm-ai-labs/turnkey-extensions": "https://github.com/Swarm-AI-Labs/turnkey-extensions/releases/download/v0.4.0/swarm.ing-turnkey-extensions-0.4.0.tgz"Do not pin git+ssh://…#vX.Y.Z. dist/ is no longer committed, so a git pin makes the app build the package itself — a DTS rollup peaking near 4.4 GB, which OOMs in a memory-capped CI container.
2. Configure at app boot
The package reads endpoints from configure() instead of NEXT_PUBLIC_*. Add one module imported once (e.g. in instrumentation-client.ts or a root layout):
import { configure } from "@swarm-ai-labs/turnkey-extensions/config";
configure({
rpc: {
sol: process.env.NEXT_PUBLIC_SOL_RPC_URL,
evm: {
1: process.env.NEXT_PUBLIC_ETH_RPC_URL,
11155111: process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL,
},
},
apiBases: {
inventory: "/api/inventory", // keep the existing Next proxy route
indexer: "", // "" ⇒ same-origin /api/price-graph
},
});This preserves today's behaviour: balances/senders use the same RPCs, and the inventory/indexer browser clients keep hitting the app's existing proxy routes.
3. Rewrite imports (domain by domain)
Replace each app module with the package subpath. Delete the app source after its consumers compile against the package.
| App file(s) to delete | Replace imports with |
|---|---|
lib/format.ts | @swarm-ai-labs/turnkey-extensions/format |
lib/validateAddress.ts | @swarm-ai-labs/turnkey-extensions/validate |
lib/catalog/cryptoNetworks.ts | @swarm-ai-labs/turnkey-extensions/catalog |
lib/near/implicitAccount.ts | @swarm-ai-labs/turnkey-extensions/signers |
components/turnkeyEip1193.ts | …/signers (createTurnkeyEip1193Provider) |
components/turnkeySolanaProvider.ts | …/signers (createTurnkeySolanaProvider) |
piecomponents/TurnkeyWalletCard/utils/prepareTurnkeyEthSend.ts, prepareTurnkeyEvmTransaction.ts | …/signers (prepareTurnkey*) |
piecomponents/WithdrawCard/senders/* | @swarm-ai-labs/turnkey-extensions/senders |
lib/holdings/balances.ts | @swarm-ai-labs/turnkey-extensions/balances |
lib/hyperliquid/* | @swarm-ai-labs/turnkey-extensions/hyperliquid |
lib/inventory/{types,client}.ts | @swarm-ai-labs/turnkey-extensions/inventory |
lib/indexer/{types,priceGraph,searchTickers}.ts | @swarm-ai-labs/turnkey-extensions/indexer |
lib/swap/quoteRefresh.ts | @swarm-ai-labs/turnkey-extensions/swap |
…/useTurnkeyAccounts.ts, useTurnkeyViemAccount.ts, useHyperliquid{Exchange,Address}.ts, chainFromAddressFormat | @swarm-ai-labs/turnkey-extensions/react |
lib/indexer/useTickerSearch.ts, useTokenPriceGraph.ts | @swarm-ai-labs/turnkey-extensions/react |
Keep in the app (not crypto-integration code):
components/TurnkeyProvider.tsx,turnkey/*Gate.tsx— kit setup/auth UI.lib/inventory/server.ts+app/api/inventory/*,app/api/price-graph/*— these are the proxy backends our browser clients call; leave them.components/AuroraWidget*.tsx— widget JSX. Swap its provider wiring tobuildAuroraTurnkeyProvidersfrom…/swap.lib/inventory/types.tsre-exports: if app code imports inventory types from the old path, re-point to…/inventory.
Tip: do it as a codemod per row — find/replace the import specifier, run
bun run typecheck, commit. One green commit per domain.
4. Map app-local types
The app passed WalletAccount, DepositChainId, ChainKey, HandleSendTransactionParams around. The package owns equivalents:
WalletAccount,HandleSendTransactionParams— re-exported from the kit by us; import from@swarm-ai-labs/turnkey-extensions/types/…/senders.DepositChainId=string(unchanged);ChainKey,EvmCaip2from…/types.ChainAccount(the hook result) — from…/react.
Delete the app's duplicate piecomponents/DepositCard/types chain aliases that now live in the package; keep app-specific UI types.
5. Wire the swap bridge
In AuroraWidgetTurnkeyBridge.tsx, replace the hand-rolled provider creation with:
import { buildAuroraTurnkeyProviders } from "@swarm-ai-labs/turnkey-extensions/swap";
const { evm, sol } = buildAuroraTurnkeyProviders({
client: httpClient,
organizationId,
evmAddress,
solanaAddress,
initialChainId,
});
// pass evm/sol into the widget's `providers` config6. Remove packages from ai-exchange-web/package.json
Delete these dependencies (now provided transitively by the package):
@mysten/sui
@near-js/crypto @near-js/providers @near-js/transactions near-api-js
@nktkas/hyperliquid
@polymarket/clob-client-v2
@scure/btc-signer
@solana/web3.js
@ton/core @ton/crypto @ton/ton
@turnkey/solana @turnkey/viem
borshKeep (app uses them directly, or they're not crypto-signing): @swarm-ai-labs/turnkey-extensions (new), viem, @turnkey/react-wallet-kit, @aurora-is-near/intents-swap-widget*, @tonconnect/ui-react, @web3icons/react, @ledgerhq/crypto-icons, algosdk (logos + swap-symbol generation only — not a Turnkey integration), plus all framework/UI deps.
Then bun install and confirm the lockfile resolves a single viem.
7. Verify & roll out
Per-domain commits, then a final pass:
bun run typecheck— clean after each domain.bun run build(opennextjs-cloudflare build) — bundles resolve.bun run build-storybook— component stories still render.- Manual smoke per chain (testnet): receive-address derivation, a small withdrawal (EVM + one ed25519 chain + one secp256k1 chain), a swap quote + sign, a Hyperliquid market read, the prediction balance.
- Watch the bundle: confirm tree-shaking kept unused chains out (no TON code on an EVM-only route).
Rollback: each step is an isolated commit; git revert restores the app's local module and its import sites. Because nothing is feature-flagged, a revert is total and immediate.
8. Risks & notes
- Single viem instance — the one hard requirement. Keep
viema peer of the package; if the app and package resolve different majors,instanceof/type checks (e.g.isAddress, account objects) break subtly. Verify withbun pm ls viem→ exactly one. @turnkey/react-wallet-kitstays app-owned — the package only types against it; the app provides the actual provider +useTurnkey().- Proxy routes stay — the inventory/indexer browser clients are CORS-safe only via the app's
/api/*routes;configure({ apiBases })points at them. algosdkis not ours — it's used for network logos and the generated swap symbol list, not Turnkey signing. It stays in the app.- Polymarket — collateral/CLOB are forward-looking; the app's prediction UI is still stubbed, so this is a drop-in for when its order backend lands.