@logswap/sdk
v0.5.85
Published
TypeScript SDK for Logswap, the liquidity primitive introducing the logarithmic invariant — typed wrappers over the CPoolManager / FPoolManager / Router / Lens surface.
Downloads
4,409
Readme
logswap-sdk
TypeScript SDK for Logswap, the liquidity
primitive introducing the logarithmic invariant — typed wrappers over the CPoolManager /
FPoolManager / LogswapRouter / LogswapLens surface.
Status: identity, reads and writes for both singletons. Indexer next.
What is here now
The two things the SDK must own because they are easy to get wrong by hand, and because getting them wrong is silent:
| module | what it does |
|---|---|
| src/keys.ts | PoolKey and poolId = keccak256(abi.encode(key)) — market identity |
| src/ids.ts | the ERC-6909 id namespace: position-class ids, token-claim ids, floor/cap packing |
| src/client.ts | the handle, plus assertVersion — refuse a deployment the ABI was not built for |
| src/pools.ts | market discovery from MarketCreated logs, pool state, lpEdge = F − Σ/2 |
| src/positions.ts | classes, balances, claimable fees, the bit-255 position/claim split |
| src/swap.ts | exact-in/out, multi-hop, and exact quotes via the revert-quoter |
| src/liquidity.ts | mint, blended mint, zap, update, move, harvest, burn |
| src/claims.ts | fee claims and the 6909 operator grant |
| src/onboard.ts | the three approvals — ERC-20 → Permit2 → router, and the 6909 grant |
Both are computed offline — no RPC, no deployment needed to address a market or a position.
Reading a holder's book
positionsOf(c, key, owner)— the uncapped book in one lens call (the ladder bitmap enumerates floors; an uncapped id is a pure function of the floor).holdingsOf(c, key, owner)— the complete book, capped classes included: replaysClassRegistered(every class is registered once, with its floor and cap) and checks the owner's balance on each. Use this for "my positions"; a capped mint is invisible topositionsOf.getHolderPosition(c, key, owner, floor, cap?)— one class by (floor, cap);move(...)/update(...)/harvest(...)/burn(...)/claimFees(...)act on it.- F side:
discoverFPools(c)(shape, and theblockthe pool was initialized in),getFPool,fPoolShareBalance,fPoolShareValue;previewZapInreads the lens (the router's F flows live in a facet, its previews moved to the lens). Since F version 25 every F state read goes through the lens (fPool,getFPools,fBundleOf,fDividendOf,fLienOf): the manager's typed views are behindextsload(decisions 062), and the lens decodes them with the contracts'FPoolState, so the SDK never hard-codes the storage layout. - F pool state names (F version 25):
swapFee(wasphi),logFloorθ — the log of the current floor price (wastheta),refLogFloorθ₀ — the reference floor income counts from (wastheta0),cap(d in 1e-6 log units, 0 uncapped), and per legvirtualReservesandkappa.
Who may create what: the gate, the menus (C version 7, F version 26)
Since logswap-contract #60 (decisions 064–065) creation is curated at launch, on both managers:
- The C gate is the public policy, set in the C manager's constructor. A C market is admitted if
its creator, quote, pair, swap-fee tier (2, 5, 10, 25, 50, 100 bp) and tick spacing (10 or 100 bp)
are listed, and only one market per (base, quote, fee) exists.
cGateRules(c, { key, creator })reads every check in one round;gateErrorAbidecodes the refusals (CreatorNotListed,MarketExists,SpacingNotListed,PairNotListed,NoForwardedUser,PadExists). createMarketstays one transaction: the router is a trusted forwarder, so the gate checks the router'suser()— the wallet that sent the call — against the creator list.- The mint fee is a key field (C 12 / F 38, decision 078): the creator's pick, fixed for the pool's life and
part of its id (
PoolKey.mintFee,FPoolKey.mintFee;mintFee?onfPoolInitialize/fPoolLaunch). The managers require2 * swapFee <= mintFee <= MINT_FEE_MAX(2%) and revertBadParam;isValidMintFee,minMintFeeandMINT_FEE_MAXare exported, andpresetMintFee(swapFee)is the least the managers accept, twice the swap fee.initialize,initializeMarketandcreateMarkettake no separate argument, the policies keep no menu, and C'sgetPoolno longer returns it (readkey.mintFee;getFPool(...).mintFeestill reads an F pool's).mintFee(p, x, rate)previews it at the market's rate. - A fee of 0 is no fee:
swapFeeOfandDEFAULT_FEEare gone;marketSwapFeereturns the key's own fee for an initialized market. - C reads:
getPoolreads the lens'sgetPools;getPositionClassreads the class throughextsloadatCPoolState's slot (the manager's typed views are gone). - Creators are curated on every kind at launch (onboarding under a service agreement); the
LOG Authority's helpers:
policySetCreatorListed,gateSetPairListed,gateSetSpacingListed,gateSetForwarderTrusted,setCGate. - Two roles (decision 066, C 8 / F 27).
logAuthority()reads the LOG Authority (the registry'slogAuthority;proposeLogAuthority/acceptLogAuthority), which administers the registry, both managers and every policy.logTreasury(side)reads a manager's treasury, which only receives;proposeTreasuryis the authority's,acceptTreasurythe incoming treasury's own call.collectCProtocolFees(key)may be sent by anyone and pays the treasury. The C swap-fee tax is each market's from birth (getPool(key).swapFeeTax,cMarketSwapFeeTax);setCSwapFeeTaxsets the gate's level for markets born after it; the C gate is fixed (setCGatethrows).registryOwner,registryProposeOwner/registryAcceptOwnerandproposeCollector/acceptCollectorremain as deprecated aliases.
History from the indexer (indexer phase 1)
Every event read goes through getEventLogs (one event) or getEventsLogs (several, tagged by
eventName): the rindexer GraphQL endpoint when one is given, the node otherwise or when it fails.
Since logswap-be #43 the indexer also holds the four policies (one Policy table per event, tracked
from the registry's Approved), the registry, the token factory and the managers' admin events, so
nothing has to scan the node from block 0 — which the devnet node, pruning logs after about a day,
answers only partly.
swapFees,holdingsOf,discoverMarketsandfPoolKeyOfPooltakeindexer(the salt is not inPoolCreated, so a pad's key is still rebuilt from its split with nonce 0).getEventLogstakes several emitters (address: [...]); a policy outside the artifact names its shared tables withcontract: indexedContractOf(kind, event)(orkindongetEventsLogs).tokensCreatedlists the factory's tokens (addresses.tokenFactory): the token list's provenance.
Indexer views (indexing phase 2)
The indexer also serves derived views (logswap-be devnet/VIEWS.md), joining its event tables with
the snapshotter's state. views.ts reads each as typed rows: amounts as bigint, prices, APYs and
progress as number, times as unix seconds. Every reader takes the GraphQL endpoint and a
{ condition, filter, orderBy, first, signal } query, and pages by offset.
| Reader | View | What a row is |
| --- | --- | --- |
| poolDirectory | allPoolDirectories | a pool of either manager, its kind and birth |
| eventUsers | allEventUsers | a manager event and who did it (the router's caller when routed) |
| poolHolders | allPoolHolders | a current ERC-6909 balance, with C's backstop and F's dead shares flagged |
| walletPositions | allWalletPositions | a wallet's holding; F valued at the latest share value |
| walletPoolFlows | allWalletPoolFlows | a wallet's in/out, fees and dividends per pool |
| swapFlows | allSwapFlows | a swap's volume and fee in quote |
| priceTicks | allPriceTicks | a price point per (pool, base) |
| candles(url, "minute" \| "hour" \| "day") | allCandle…s | OHLC with volume and fees |
| poolTvlHours | allPoolTvlHours | the hour's last snapshot: TVL, liquidity |
| poolSummaries | allPoolSummaries | 24 h / 7 d volume, fees, APY; latest TVL |
| generationSteps | allGenerationSteps | a generation step and the split of the income it released |
| padStatuses | allPadStatuses | each pad and POL now, with the next threshold and progress |
| protocolRevenueDays | allProtocolRevenueDays | protocol revenue per day and source |
Given an indexer, three functions now take their figure from a view and keep their previous path
as the fallback, with the same answer on the same data:
holdingsOf: the owner's held class ids frompool_holder, then the lens as before;fPoolShareHolders: the holders frompool_holder;swapFees: the window fromswap_flow.
What stays a live read: a C position's value and every unclaimed fee or dividend (the lens: legsOf,
heldByIds, fDividendOf). They accrue without events, so no view holds them.
The F pool's life: four calls (F 38)
fPoolInitializecreates an unfunded pool. Its key is(quote, bases, swapFee, policy, creator, salt, mintFee): no weights and no cap, and the set ofbasesis fixed for the pool's life.fPoolDeploy(deployLiquidity, decisions 083, 084) is the authority's and SETS THE BOOK. EachbaseAssets[j]is the TOTAL base reserve of assetjafter the call (0leaves it out) and the price that values it (priceX36),quoteAmountis the total bid, and the manager settles the difference with the caller: a larger total is pulled, a smaller one is pushed toto, so one call replaces an asset by another. Pass the book outright (baseAssets,lnCapOverSeed) or the sponsor's terms (bases,weights,L0and the wanted marksx0or the reservesr0;fPoolBookbuilds the book). On an UNFUNDED pool (new, or afterfPoolClose) it funds the first or next generation. On a LIVE pool it is a regeneration that sets the book: income settles toto, every holder but the carried ones is frozen into the closing generation's bundle (fPoolRedeem), and the carried book becomes the one named. A pad is forward-only (PadForwardOnly): it usesfPoolRegenerate.fPoolRegenerateis the same routine with nothing changed: the generation step, the pad's graduation.fPoolCloseharvests the bid out and closes the pool at an empty bid; the pool keeps its id and every role.The cap (decisions 061, 075) is the pool's one log ratio
lnCapOverSeed= ln(cap price / seed price) in 1e-6 log units, chosen at each deployment: each asset's maximum price is its own seed price times it.fPoolCapOfLeverage(λ)gives it for a leverage (λ = 2 → the cap at twice the seed price),fPoolLeverageOfCap(d)the inverse, andfPoolCapK(d)= e^{−d} the constant that scales each reserve by (1 − k) and divides the base value by (1 − k) to give the liquidity. A pad is uncapped (PadUncapped).LiquidityDeployedechoes the deployment:(quoteAmount, baseAssets, lnCapOverSeed).fPoolBookWeightsandfPoolBookLderive the allocation and the liquidity from it. A regeneration emits no such event; the freeze-out is read fromGenerationRetiredandIncomeReleased.Income and income tax (decision 085): the dead shares' dividend and the share of it the treasury takes are the income and the income tax (
IncomeReleased,incomeHeld,incomeTax,padSetIncomeTaxRate).The lien (decisions 063): a lending contract locks an owner's shares in place.
fPoolApproveShares(the owner's per-id allowance),fPoolLock/fPoolUnlock(the locker's;to= owner is a release, anyone else a seizure),fPoolBankLien(bank the locked dividend now),fPoolLienOf(locker, amount, and the dividend the lien holds). The dividend the locked amount earns is held by the lien: a repaid loan gets it back, a seizer takes it.
Chains and deployments (F version 29)
log(swap) releases on Ethereum (1), Base (8453), Arbitrum One (42161) and Robinhood Chain (4663), with Robinhood Chain Testnet (46630) for testing. The contracts are deployed through CreateX (CREATE3, contracts decision 069) at the SAME addresses on every chain, so an address names a contract only together with a chain id.
LOGSWAP_CHAINS(src/chains.ts): one entry per chain — the viemchain,network(the artifact's),nativeCurrency,explorer,publicRpc(rate-limited),devnet,testnet, the verifiedquotes(symbol, address, decimals) andpadQuote, andmaxLogRange. Also 31337 (local anvil) and 31339 (the devnet).logswapChain(id),quoteOf(id, "USDC"),RELEASE_CHAIN_IDS.LogswapDeploymentis the contracts'deployments/<chainId>.json(DeployChain.s.sol; the localDeployLocalartifact has the same fields plus its devnet extras).deploymentFromArtifact(json, { chainId })validates it and refuses an artifact for another chain;LOGSWAP_DEPLOYMENTS/deploymentFor(id)hold the published ones (none yet: nothing is deployed on the release chains).createLogswapClient({ public, deployment, indexerUrl })takes the addresses from the deployment and keeps itschainIdanddeployBlock;addressesstill works on its own.
import { createPublicClient, http } from "viem";
import { createLogswapClient, deploymentFromArtifact, logswapChain } from "@logswap/sdk";
const chain = logswapChain(8453)!;
const c = createLogswapClient({
public: createPublicClient({ chain: chain.chain, transport: http(process.env.BASE_RPC_URL) }),
deployment: deploymentFromArtifact(artifactJson, { chainId: 8453 }),
indexerUrl: "https://…/graphql", // this chain's indexer
});What changes for readers:
Log scans start at
deployBlockwhere they started at block 0 (discoverMarkets,discoverFPools,fPoolKeyOfPool,holdingsOf,fPoolShareHolders,getEventLogs,getEventsLogs); an explicitfromBlockstill wins.The chain path is chunked:
eth_getLogsover spans ofmaxLogRangeblocks (2 000 on the release chains, per-client override), at most four in flight (getLogsChunked). Public RPCs refuse an unbounded range.The indexer is per chain:
indexerUrlon the client is the defaultindexerof every history read and ofdiscoverMarketsBest. Its rows are filtered by contract address, which is the same on every chain, so one URL must serve one chain.Caches key on the chain id: the policy-kind cache asks the node for the chain id when the client has none;
RESETTABLE_CHAINSis every registry chain flaggeddevnet(plus 1337 and 31338), extended withaddResettableChain.F version 29 (decision 068):
FPoolState.restructureTime(unix seconds; wasrestructureBlock),registrySunset(c, policy, untilTime)andRegistryEntry.untilare timestamps.EXPECTED_F_VERSIONis 29.F version 31 (decision 072):
DividendClaimedcarries the generation (gen, indexed);lockandunlockemitLienLocked/LienUnlocked, so a lien is readable from the log stream withgetEventLogs.EXPECTED_F_VERSIONis 31.
Conventions
- The SDK hides the manager/router/lens split. Which contract hosts which action is currently decided by EIP-170 headroom, not by design — so a future re-homing must be an SDK patch, not a breaking change for consumers.
- Zero is not the default. A
swapFeeof0n(phibefore C version 6) resolves toDEFAULT_FEEat read time, so a key carrying0nand one carrying the resolved value are different markets with different ids. Always address a market by the key it was initialized with. - Split every ERC-6909
Transferon bit 255. Clear means a token claim, not a position; an indexer that ignores this reports phantom LP positions for market makers parking inventory.
Install
npm i @logswap/sdk viemPublished as @logswap/sdk from v0.5.35; earlier versions used another name.
viem is a peer dependency. The package ships dist/ plus src/, including the generated ABIs, so
the contract surface it was built against travels with it.
