Skip to content

@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 time

Because 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:

ts
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 Chain SDKs ship with the package ​

Installing this package installs every chain SDK it can reach: viem, @solana/web3.js, @near-js/*, near-api-js, @ton/*, @mysten/sui, tronweb, @scure/*, @noble/curves, borsh, @nktkas/hyperliquid, @polymarket/clob-client-v2, @turnkey/* and the 0G compute SDK. They are dependencies, so a consumer names none of them.

Only the surfaces an application supplies for itself are optional peers, and they are marked so in peerDependenciesMeta: react and react-dom, @turnkey/react-wallet-kit, the Aurora widgets, and openai.

That the SDKs INSTALL does not mean they LOAD. Nothing is imported until a call touches it, and the unbundled output below is what keeps an unused chain out of a consumer's bundle. Install cost and bundle cost are separate questions here.

1.4 Unbundled output — pay for what you import ​

"sideEffects": false plus an UNBUNDLED build (one output file per source file, see tsup.config.ts) is what lets a consumer's bundler drop what it does not reach. Bundled output merged unrelated modules into shared chunks, and a bundler can only drop a whole chunk — which is how importing the perps surface used to drag @solana/web3.js into an app's graph.

The exports map names eight entry points, not one per domain: ./swarm, ./react, ./types, ./compute, ./display, ./server, ./testing and ./runtime/node. Reaching a chain still costs only that chain, because the module graph inside those entries is at file granularity.


2. Module map ​

Forty modules under src/, grouped by what they are for.

The vocabulary every layer shares ​

config.ts        configure() / getConfig() / resetConfig() + mainnet defaults
types.ts         DepositChainId, ChainKey, EvmCaip2, WalletAccount
chains.ts        EVM slug→id, native symbols/decimals, labels
memo.ts          the SWARM1 tag a write emits and a read recognises
format.ts        formatUsd, splitCurrency, formatSignedPercent, signColor
cache.ts         the promise cache every TTL'd read shares
turnkeyErrors.ts Turnkey's activity failures, in words

Each is a leaf on purpose: they are the shared vocabulary of domains that must not import each other. memo.ts is the clearest case — swap writes the tag, activity reads it, and owned by either one those two would form a cycle.

Chain access ​

catalog/       EVM network catalog: RPC resolver, explorer + logo URLs
validate/      isValidAddressForNetwork — per-chain recipient checks
signers/       Turnkey signing adapters: EIP-1193, EIP-3009, EVM prep, ed25519
senders/       per-chain NetworkSender (prepare → confirm) + factory
evm/           RPC resolution and EVM-side helpers
internal/      loggedFetch, fetchWithRetry, the observed viem transport

Reading the chain ​

balances/      fetchers.ts (one per chain+standard), portfolio routing, overlay
prices/        CoinGecko + Sparkling indexer, behind one provider
indexer/       ticker search + token price-graph clients
inventory/     Sparkling inventory: chains, deployments, resolution
activity/      per-chain mappers, fetchers, decoration, history
holdings/      valuation and list-view rules

Venues ​

hyperliquid/   perps: assets, account, book, order math, exchange, trade
polymarket/    prediction markets: CLOB, collateral, Safe/deposit wallets
adi/           ADI PredictStreet: markets with per-user on-chain vaults
cex/           centralised venues (Binance), signed queries
cow/           CoW Protocol limit orders
swap/          1Click, Jupiter, Bebop, Uniswap v3, and the deposit legs
zgcompute/     0G Compute: broker, ledger, inference
venue/         one adapter shape over all of them, and the sweep across

Deciding and acting ​

runtime/       identity · policy · store · ops · products — the session
execution/     TWAP/ladder/iceberg slicing and the journal a resume reads
strategy/      recipes, backtesting
signals/       Envy Trading feed and the trades it implies
alerts/        conditions, edge detection, delivery
risk/          standing limits
paper/         simulated fills
screen/        market screening
chart/         candles for a terminal

Surfaces ​

domain/        the vocabulary an application names        → ./types
compute/       arithmetic and predicates, no I/O          → ./compute
display/       what a screen shows                        → ./display
react/         hooks and providers                        → ./react
server/        the half that runs on a server only        → ./server
testing/       fakeSwarm — a session without a wallet     → ./testing
observability/ events, spans, sinks
progress/      live status for whoever is waiting
wallet/        allowances and the risk ranking over them

Dependency direction ​

the root leaves (config, chains, memo, format, …)
        ▲
catalog · internal · validate · signers · senders
        ▲
balances · prices · indexer · inventory · activity · holdings
        ▲
hyperliquid · polymarket · adi · cex · cow · swap · zgcompute · venue
        ▲
runtime (identity · policy · store · ops · products)
        ▲
react · cli · server

The package has no import cycles. src/swap/deposit/signer.ts is the one edge that runs the other way: it builds a viem account through runtime/identity, which sits above it. It is a single import of a single shim and forms no cycle, but it is the one place the layering above is not literally true.

3. Public API (subpath exports) ​

Import pathContents
@swarm-ai-labs/turnkey-extensionsroot barrel (everything below except polymarket/react)
…/configconfigure, getConfig, resetConfig, config types
…/typesDepositChainId, ChainKey, EvmCaip2, WalletAccount
…/formatformatUsd, splitCurrency, formatSignedPercent, signColor
…/validateisValidAddressForNetwork
…/catalogPOPULAR_EVM_NETWORKS, findEvmNetwork, getDefaultEthRpcUrlForChain, …
…/signerscreateTurnkeyEip1193Provider, createTurnkeySolanaProvider, near*, prepare*, TurnkeySigning
…/senderssenderForNetwork, the 7 senders, toBaseUnits/fromBaseUnits, explorerTxUrl
…/balancesfetch*Amount, fetchPricesForSymbols, COINGECKO_IDS
…/hyperliquidresolvePerpAsset, fetchPerp{Market,Markets,Account,Book}, buildPerpOrderPlan, placePerpOrder, closePerpPosition, setPerp{Leverage,TpSl}, bridge + order-math helpers
…/polymarketgetPolymarketClobClient, polymarketCollateralAddress, fetchPredictionUsdcBalance
…/inventorysearchTicker, loadInventoryUniverse, resolveDeploymentForSymbol, …
…/indexersearchTickers, getTokenPriceGraph
…/swapQUOTE_REFRESH_MS, buildAuroraTurnkeyProviders
…/reactall 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 + signTransaction

4.3 Balances ​

fetchEvmNativeAmount(chainId, addr)
   └▶ cachedBalance(key, 30s) ─▶ rpc(configRpc, "eth_getBalance") ─▶ wei→number
fetchPricesForSymbols([...])  ─▶ COINGECKO_IDS ─▶ config coingecko base (60s cache)

5. Configuration ​

ts
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 ​

ts
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) ​

ts
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 widget

Read balances + prices ​

ts
import {
  fetchSolAmount,
  fetchErc20Amount,
  fetchPricesForSymbols,
} from "@swarm-ai-labs/turnkey-extensions/balances";

const sol = await fetchSolAmount(address);
const { SOL, USDC } = await fetchPricesForSymbols(["SOL", "USDC"]);

Hyperliquid ​

ts
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 ​

tsx
import {
  useTurnkeyAccounts,
  useHyperliquidAddress,
} from "@swarm-ai-labs/turnkey-extensions/react";

const accounts = useTurnkeyAccounts({ includeSolana: true });
const hl = useHyperliquidAddress();

7. Extending: add a chain ​

  1. src/senders/<chain>.ts implementing NetworkSender (prepare → confirm), using client.signMessage / signAndSendTransaction for the Turnkey signature and a config-driven endpoint accessor in shared.ts for broadcast.
  2. Register it in senderForNetwork() (senders/index.ts).
  3. Add native decimals/symbol/label to chains.ts and a case to validate/.
  4. If it needs a new SDK, add it as an optional peer dep + external in tsup.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 ​

CommandWhat it does
bun run typechecktsc --noEmit (strict)
bun run testvitest — pure-function unit tests + the swap signing-bridge e2e
bun run buildtsup → dual ESM + CJS + .d.ts for all 15 entry points
bun run lintprettier + 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.ts drives buildAuroraTurnkeyProviders across the entire EIP-1193 surface (accounts, chain switch, personal_sign, eth_signTypedData_v4, full eth_sendTransaction broadcast) 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.

Released under the MIT License.