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 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 domain

Dependency 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 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.