npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@calamari-dex/sdk

v0.0.2

Published

TypeScript SDK for Calamari: quote, swap, and manage liquidity on the Calamari v4 DEX.

Downloads

394

Readme

@calamari-dex/sdk

TypeScript SDK for Calamari — quote, swap, and manage liquidity on the Calamari v4 DEX.

Quick start

import { CalamariClient, createPoolKey, poolId, NATIVE } from "@calamari-dex/sdk";
import { createPublicClient, http } from "viem";
import { inkSepolia } from "viem/chains";

const publicClient = createPublicClient({ chain: inkSepolia, transport: http(RPC) });
const calamari = new CalamariClient({ publicClient });

const pool = createPoolKey({
  currencyA: NATIVE,          // sorting is handled for you
  currencyB: USDC,
  fee: 3000,
  tickSpacing: 60,
});

const state = await calamari.getPoolState(poolId(pool));
const quote = await calamari.quoteExactInputSingle(pool, true, 10n ** 15n);
const [tx] = await calamari.prepareSwapExactInSingle(pool, true, 10n ** 15n);

Why this is viem-native

Calamari runs unmodified Uniswap v4 — v4-core, v4-periphery and permit2 are vendored as clean upstream submodules, and the only bespoke contract is a thin V4Router subclass wiring Permit2 payment.

This package used to re-export @uniswap/v4-sdk's entity layer, on the reasoning that the encoding format is not ours to define. That was wrong twice over.

It was wrong on cost. Everything this SDK needs from that layer is a five-field PoolKey, a keccak, and an ABI encoder — it does no tick math, no liquidity math, no routing. import { Pool } alone pulls 981KB, because the barrel export reaches v4BaseActionsParser, which imports all of ethers v5. That is a JSON-RPC provider and a secp256k1/AES signing stack inside a package whose whole premise is that it never signs or sends anything, sitting next to viem, which already does all of it.

It was wrong on correctness, which matters more. The struct a router decodes is a property of your deployment, and an upstream encoder versions on its own schedule. V4Planner.addAction defaults to the pre-minHopPriceX36 struct; the router here reads the one with it. The mismatch did not revert — it slipped the decoder's params.length < 0x160 guard by exactly one word, read the hookData offset as a price bound, then read hookData's length off currency0. Native swaps survived because that address is zero. Every ERC20-to-ERC20 swap did not.

So the encoding is built here, against ABIs generated from the deployed artifacts, and src/actions.ts is generated from Actions.sol. Nothing describing the protocol is hand-written, and nothing versions independently of what is deployed.

For tick and price math, or position sizing, use @uniswap/v4-sdk alongside this package. That is pure math, independent of any deployment, and it composes with the plain types here.

Chain types

CalamariClient is generic over the chain and infers it from the client you pass, so calamari.publicClient hands back exactly what you gave it. That is load-bearing rather than cosmetic: every OP-stack chain — Ink included — ships formatters that widen getBlock's transaction union with a "deposit" type, and a client typed with one of them does not assign to a bare PublicClient. Taking the wide type would have red-lined the snippet above on its first line with TS2719. test-types/consumer.ts compiles these shapes on every npm run typecheck so it cannot come back.

Addresses

src/addresses.ts is generated from deployments/*.json, never written by hand. Regenerate after any deploy:

forge build && npm run generate

Contracts absent from some chains are typed optional, so consumers handle the gap instead of reading undefined off a type that promised an address. Which keys are optional is derived, not declared: a key present on every deployment is required, and the type changes when that stops being true. router (the InkV4Router) and positionDescriptor are currently the optional ones — both anvil-only.

The books are frozen. client.addresses is the shared module singleton, so an unfrozen one would let a single consumer's mutation reach every client in the process.

Swaps

All four shapes, each with a matching quote:

await calamari.prepareSwapExactInSingle(pool, zeroForOne, amountIn, opts);
await calamari.prepareSwapExactOutSingle(pool, zeroForOne, amountOut, opts);
await calamari.prepareSwapExactIn(route, amountIn, opts);   // multi-hop
await calamari.prepareSwapExactOut(route, amountOut, opts); // multi-hop

slippageBps means opposite things either way round, which is the whole difference between them. Exact-input lowers a floor on what you receive; exact-output raises a ceiling on what you pay.

Two consequences of exact-output worth knowing. The pool takes only what the output actually costs, so a native input overpays by construction: the ceiling goes out as msg.value, and the remainder comes back via the UniversalRouter's SWEEP command, appended automatically. It has to be the router's command rather than the v4 SWEEP action, because V4Router does not implement that action and rejects it with UnsupportedAction — and the overpay is held by the router, not the PoolManager. Left behind it would belong to whoever swept next.

Multi-hop takes a Route from createRoute(currencyIn, pools, hookData?), which walks the pools to derive the output currency — a list that does not actually connect fails there rather than as an opaque on-chain revert. Only the two ends are settled; intermediate currencies net out inside the same unlock. Hook data is per hop, so it goes to createRoute, not to the swap.

Per-hop price bounds

The deployed V4Router carries minHopPriceX36 on every swap struct. This SDK sends 0 (single) and [] (multi), which the router reads as "no per-hop check"; the aggregate amountOutMinimum still bounds the swap end to end. A multi-hop array must be either empty or exactly one entry per hop — InvalidHopPriceLength otherwise.

That field is also why these builders pass URVersion.V2_1_1 to the planner. V4Planner defaults to the older 2.0 struct, which omits it, and the older encoding still decodes against the deployed router rather than reverting: it reads the hookData offset as the price bound and drops hookData entirely. The quote keeps the hook data and the swap loses it, so on a hooked pool the two stop agreeing. Encode with the version the deployment actually runs.

Creating a pool

prepareCreatePool reads the pool's state before building anything, because the transaction itself cannot report failure: PositionManager.initializePool wraps poolManager.initialize in a try/catch and returns type(int24).max rather than reverting. Creating a pool that already exists therefore succeeds and leaves the existing price untouched — so a create-then-seed batch would add liquidity at whatever price the first caller chose, with nothing to signal it. prepareCreatePoolUnchecked skips the read for cases where the pool provably cannot exist yet.

Permit2 approvals

Swap and liquidity builders do not automatically include approvals. Call prepareApprovals({ owner, token, flow, amount }) first: it reads both allowances and returns only the missing approval transactions. Use flow: "swap" for swaps and flow: "liquidity" for liquidity. Send the returned transactions in order and wait for confirmation before submitting the trade. Native inputs return an empty approval list.

ERC20 inputs require two grants, and the spender differs by flow:

// once per token
token.approve(PERMIT2, MAX_UINT160)

// swapping: the router spends
permit2.approve(token, universalRouter, amount, expiration)

// adding liquidity: the PositionManager spends
permit2.approve(token, positionManager, amount, expiration)

A mint reverts if only the router grant exists, and a swap reverts if only the PositionManager grant does. For explicit control, use prepareErc20Approve and preparePermit2Approve; erc20Abi and permit2Abi are also exported. Native inputs need neither — the value rides along on the transaction.

What is not here

No tick math, no price conversion, no position sizing, and no path-finding. The builders take ticks and liquidity from you. For that math use @uniswap/v4-sdk alongside this package: it is pure and deployment-independent, and composes with the plain PoolKey type here.

Wallets the SDK cannot drive

Privy and other embedded or MPC signers never hand over a private key, so there is no viem WalletClient to give the SDK. prepare* builds the transactions and stops there:

const txs = await calamari.prepareSwapExactInSingle(pool, true, amountIn);

for (const tx of txs) {
  await privyWallet.sendTransaction({ to: tx.to, data: tx.data, value: tx.value });
}

Send them in order and wait for each to confirm.

Regenerating

forge build && npm run generate   # ABIs from out/, addresses from deployments/

Build

dist/ is plain tsc output — no bundler. There is nothing to inline: the package has zero runtime dependencies, with viem as a peer. smoke.mjs asserts that, because a runtime dependency creeping back is what would force bundling again.

License

MIT (see LICENSE), with no third-party code inlined. Uniswap's packages are devDependencies only — used by src/differential.test.ts to prove this encoder is byte-identical to theirs — so npm handles their attribution normally and there is no notice file to ship.

The ABIs in src/abis/ are generated from contracts under several licenses, which are not all MIT:

| Artifact | Source | License | | --- | --- | --- | | poolManagerAbi | v4-core | BUSL-1.1 until 2027-06-15, then MIT | | universalRouterAbi | universal-router | GPL-3.0-or-later | | positionManagerAbi, stateViewAbi, quoterAbi | v4-periphery | MIT | | permit2Abi | permit2 | MIT | | routerAbi | src/InkV4Router.sol (ours) | MIT |

An ABI is an interface description rather than the implementation, and is generally treated as unprotected — but the blanket "MIT" this file once claimed was wrong. The deployment side is a separate question: v4-core is BUSL-1.1 until 2027-06-15, and production use before then needs the Additional Use Grant at v4-core-license-grants.uniswap.eth or a license from Uniswap Labs.