@swarm-ai-labs/turnkey-extensions — Architecture & Usage
A standalone, framework-agnostic library that holds every crypto integration of the ai-exchange-web wallet: cross-chain Turnkey signing & sending, address validation, multi-chain balances/prices, Hyperliquid perps, Polymarket collateral, the inventory/indexer data clients, the Aurora swap bridge, and optional React bindings.
1. Design pillars
The whole architecture follows from four rules.
1.1 Configuration injection — no host environment
The source app read endpoints from NEXT_PUBLIC_* env vars. A reusable library can't. Every RPC/API endpoint is resolved through a single config module with mainnet defaults baked in:
configure(partial) ──writes──▶ module state ◀──reads── getConfig()
▲
every domain reads endpoints here, at call timeBecause endpoints are read at call time (not import time), configure() can run after the domains are imported and overrides still apply.
1.2 Turnkey decoupling — structural, not nominal
The senders/signers never depend on the concrete kit at runtime. TurnkeySigning is a type-only Pick<> of @turnkey/react-wallet-kit's client methods:
export type TurnkeySigning = Pick<
TurnkeyClientMethods,
"signMessage" | "signTransaction" | "signAndSendTransaction"
>;The import is erased at build → zero runtime coupling. Any client that structurally matches works. The concrete kit appears only under ./react.
1.3 Optional peer dependencies — install only what you use
Only viem, @noble/curves, @scure/*, borsh are hard dependencies. Every chain SDK (@solana/web3.js, @near-js/*, @ton/*, @mysten/sui, tronweb, @nktkas/hyperliquid, @polymarket/clob-client-v2, @turnkey/*, react) is an optional peerDependency.
1.4 Lazy subpaths — pay for what you import
"sideEffects": false + one subpath export per domain means importing ./hyperliquid never loads TON/Sui/NEAR, and non-React consumers never load React. The bundle only includes the chains a consumer actually touches.
2. Module map
src/
config.ts configure() / getConfig() / resetConfig() + mainnet defaults
types.ts DepositChainId, ChainKey, EvmCaip2, WalletAccount (from kit)
chains.ts EVM_CHAIN_IDS, native symbols/decimals, labels (shared metadata)
format.ts formatUsd, splitCurrency, formatSignedPercent, signColor
validate/ isValidAddressForNetwork — per-chain recipient checks
catalog/ EVM network catalog: RPC resolver, explorer + logo URLs
signers/ Turnkey signing adapters (the heart)
turnkeyTypes TurnkeySigning interface (decoupled)
eip1193 createTurnkeyEip1193Provider — full EVM EIP-1193
solanaProvider createTurnkeySolanaProvider
near implicit-account derivation (ed25519 → NEAR)
prepareEvmTx nonce/gas/EIP-1559 builders + balance check
senders/ per-chain NetworkSender (prepare → confirm) + factory
shared unit conversions, ed25519 sig assembly, explorer URLs,
config-driven endpoint accessors
evm bitcoin solana tron ton near sui
index senderForNetwork(slug)
balances/ multi-chain native + token reads
internal fetchWithRetry, cachedBalance (TTL), rpc, weiToNumber
prices COINGECKO_IDS, fetchPricesForSymbols
index fetch{Evm,Btc,Sol,Near,Tron,Ton,Sui}Amount + token variants
hyperliquid/ client (Info/Exchange), assets (ticker→index), account (positions,
orders, fills, funding, per-asset leverage), perps (market data),
bookFill (walk the book), orderMath (sizing, liquidation, funding),
exchange (PerpExchange seam + cloids), trade (pure plan →
place/close/cancel/modify/leverage/margin/TP-SL/dead-man),
bridge, withdraw, subscriptions (WebSocket feeds)
polymarket/ CLOB client factory + Polygon USDC.e collateral + balance
inventory/ Sparkling inventory: types, config-driven request, client
indexer/ ticker search + token price-graph clients
swap/ quoteRefresh + buildAuroraTurnkeyProviders (Aurora bridge)
react/ hooks: useTurnkeyAccounts, useTurnkeyViemAccount,
useHyperliquid{Exchange,Address}, useTickerSearch,
useTokenPriceGraph, chainFromAddressFormat
index.ts root barrel — every non-React, non-Polymarket domainDependency direction (no cycles)
config ─▶ (everyone)
catalog ─▶ config
chains ─▶ catalog
validate, senders, balances ─▶ chains
signers ─▶ catalog, config
senders ─▶ signers, chains, config
swap ─▶ signers
polymarket ─▶ balances (subpath-only)
react ─▶ signers, hyperliquid, indexer (subpath-only)polymarket and react are subpath-only (excluded from the root barrel): Polymarket re-exports enums (Chain, Side) that would collide with the inventory Chain type, and React must stay opt-in.
3. Public API (subpath exports)
| Import path | Contents |
|---|---|
@swarm-ai-labs/turnkey-extensions | root barrel (everything below except polymarket/react) |
…/config | configure, getConfig, resetConfig, config types |
…/types | DepositChainId, ChainKey, EvmCaip2, WalletAccount |
…/format | formatUsd, splitCurrency, formatSignedPercent, signColor |
…/validate | isValidAddressForNetwork |
…/catalog | POPULAR_EVM_NETWORKS, findEvmNetwork, getDefaultEthRpcUrlForChain, … |
…/signers | createTurnkeyEip1193Provider, createTurnkeySolanaProvider, near*, prepare*, TurnkeySigning |
…/senders | senderForNetwork, the 7 senders, toBaseUnits/fromBaseUnits, explorerTxUrl |
…/balances | fetch*Amount, fetchPricesForSymbols, COINGECKO_IDS |
…/hyperliquid | resolvePerpAsset, fetchPerp{Market,Markets,Account,Book}, buildPerpOrderPlan, placePerpOrder, closePerpPosition, setPerp{Leverage,TpSl}, bridge + order-math helpers |
…/polymarket | getPolymarketClobClient, polymarketCollateralAddress, fetchPredictionUsdcBalance |
…/inventory | searchTicker, loadInventoryUniverse, resolveDeploymentForSymbol, … |
…/indexer | searchTickers, getTokenPriceGraph |
…/swap | QUOTE_REFRESH_MS, buildAuroraTurnkeyProviders |
…/react | all hooks |
4. Data flows
4.1 Native withdrawal (EVM example)
caller ─▶ senderForNetwork("ethereum") ─▶ evmSender
evmSender.prepare(input)
└▶ prepareTurnkeyEthSendTransaction() ── viem public client (config RPC)
nonce + gasLimit×1.2 + EIP-1559 fees + balance check
◀── PreparedSend { feeLabel, networkLabel, confirm() }
prepared.confirm()
└▶ handleSendTransaction(prepared) ── Turnkey kit signs + broadcasts
◀── SendResult { txHash, explorerUrl }Non-EVM senders broadcast themselves: build tx → client.signMessage / signAndSendTransaction (Turnkey) → POST to the chain's RPC.
4.2 Swap signing bridge (Aurora)
The widget owns the quote + tx building; the library owns the signing providers.
buildAuroraTurnkeyProviders({ client, evmAddress, solanaAddress })
├─ evm: createTurnkeyEip1193Provider(...) widget calls provider.request(...)
│ eth_accounts │ eth_chainId │ wallet_switchEthereumChain
│ personal_sign │ eth_signTypedData_v4
│ eth_sendTransaction → nonce/gas/fees → Turnkey sign → RPC broadcast
└─ sol: createTurnkeySolanaProvider(...) publicKey + signMessage + signTransaction4.3 Balances
fetchEvmNativeAmount(chainId, addr)
└▶ cachedBalance(key, 30s) ─▶ rpc(configRpc, "eth_getBalance") ─▶ wei→number
fetchPricesForSymbols([...]) ─▶ COINGECKO_IDS ─▶ config coingecko base (60s cache)5. Configuration
import { configure } from "@swarm-ai-labs/turnkey-extensions/config";
configure({
rpc: {
sol: process.env.SOL_RPC_URL,
near: process.env.NEAR_RPC_URL,
evm: { 1: process.env.ETH_RPC_URL, 8453: process.env.BASE_RPC_URL },
},
// Empty ⇒ same-origin proxy paths (/api/...). Set to call upstreams directly.
apiBases: { inventory: "/api/inventory", indexer: "" },
});Defaults: Solana mainnet-beta, fastnear, TronGrid, toncenter v2/v3, Sui mainnet, mempool.space, CoinGecko, inventory-stage. Call once at startup; safe to layer.
6. Usage
Send the native coin via Turnkey
import { senderForNetwork } from "@swarm-ai-labs/turnkey-extensions/senders";
import { isValidAddressForNetwork } from "@swarm-ai-labs/turnkey-extensions/validate";
if (!isValidAddressForNetwork("solana", to)) throw new Error("bad address");
const sender = senderForNetwork("solana"); // → solanaSender
const prepared = await sender!.prepare({
account,
toAddress: to,
amount: "0.1",
symbol: "SOL",
network: "solana",
client /* TurnkeySigning */,
handleSendTransaction,
organizationId,
});
console.log(prepared.feeLabel); // "≈ 0.000005 SOL"
const { txHash, explorerUrl } = await prepared.confirm();Wire a swap (Aurora widget providers)
import {
buildAuroraTurnkeyProviders,
QUOTE_REFRESH_MS,
} from "@swarm-ai-labs/turnkey-extensions/swap";
const providers = buildAuroraTurnkeyProviders({
client,
organizationId,
evmAddress,
solanaAddress,
initialChainId: 1,
});
// feed providers.evm / providers.sol + refetchQuoteInterval: QUOTE_REFRESH_MS into the widgetRead balances + prices
import {
fetchSolAmount,
fetchErc20Amount,
fetchPricesForSymbols,
} from "@swarm-ai-labs/turnkey-extensions/balances";
const sol = await fetchSolAmount(address);
const { SOL, USDC } = await fetchPricesForSymbols(["SOL", "USDC"]);Hyperliquid
import {
fetchPerpMarket,
fetchPerpAccount,
resolvePerpAsset,
fetchPerpFeeRates,
buildPerpOrderPlan,
placePerpOrder,
validateDepositAmount,
} from "@swarm-ai-labs/turnkey-extensions/hyperliquid";
const market = await fetchPerpMarket("BTC");
const v = validateDepositAmount({ amount: 10, walletUsdc: 50 }); // { ok: true }
// Planning is pure and throws `PerpOrderError` for anything the exchange would
// reject — the size flooring, the $10 minimum, the collateral, the leverage
// ceiling and a trigger on the wrong side of the entry are all settled before a
// signature exists. `placePerpOrder` sets the leverage first when the plan needs
// it, then sends the entry and any attached TP/SL as one grouped order.
const asset = (await resolvePerpAsset("BTC"))!;
const plan = buildPerpOrderPlan(
{ coin: "BTC", side: "buy", marginUsd: 100, leverage: 5, sl: 60_000 },
{
asset,
markPx: market!.markPx,
withdrawableUsd: (await fetchPerpAccount(address)).withdrawableUsd,
currentLeverage: 20,
currentMarginMode: "cross",
feeRates: await fetchPerpFeeRates(address),
},
);
await placePerpOrder({ plan, wallet });React
import {
useTurnkeyAccounts,
useHyperliquidAddress,
} from "@swarm-ai-labs/turnkey-extensions/react";
const accounts = useTurnkeyAccounts({ includeSolana: true });
const hl = useHyperliquidAddress();7. Extending: add a chain
src/senders/<chain>.tsimplementingNetworkSender(prepare → confirm), usingclient.signMessage/signAndSendTransactionfor the Turnkey signature and a config-driven endpoint accessor inshared.tsfor broadcast.- Register it in
senderForNetwork()(senders/index.ts). - Add native decimals/symbol/label to
chains.tsand a case tovalidate/. - If it needs a new SDK, add it as an optional peer dep +
externalintsup.config.ts.
Because Turnkey signs at the curve level (secp256k1 / ed25519), any chain on those curves is reachable — it only needs its own tx-build + broadcast adapter.
8. Build, test, release
| Command | What it does |
|---|---|
bun run typecheck | tsc --noEmit (strict) |
bun run test | vitest — pure-function unit tests + the swap signing-bridge e2e |
bun run build | tsup → dual ESM + CJS + .d.ts for all 15 entry points |
bun run lint | prettier + eslint |
CI (.github/workflows/ci.yml) runs typecheck → test → build on push/PR.
Test strategy
- Unit — pure helpers: validation, formatting, config merge, unit conversions, ed25519 assembly, order math, bridge math, NEAR derivation, Polymarket collateral.
- E2E (swap) —
src/swap/swap.e2e.test.tsdrivesbuildAuroraTurnkeyProvidersacross the entire EIP-1193 surface (accounts, chain switch,personal_sign,eth_signTypedData_v4, fulleth_sendTransactionbroadcast) and the Solana provider, with Turnkey (@turnkey/viem,@turnkey/solana) and the RPC (viem public client) mocked at the boundaries — every line between the widget call and those boundaries is real library code.
src/swap/swap.e2e.test.ts ✓ (8 cases)Network-touching readers (balances, inventory, indexer) are designed for fetch mocking; on-chain broadcast is asserted at the provider boundary rather than against a live chain.