Skip to main content
dlmm-pro-commons is a Rust crate for off-chain DLMM Pro integrations, aimed at aggregators and quoting services. It parses DLMM Pro accounts and quotes exact-in swaps with a port of the program’s swap loop, including the dynamic fee, limit order fills and Token 2022 transfer fees. It has no RPC client and no instruction builders. You fetch accounts however you like and build instructions from the generated dlmm_pro::client types.

Installation

The crate is not published to crates.io. Depend on it from git, pinned to a commit:
That commit is dlmm-pro-commons 0.1.1, built for DLMM Pro program 0.1.0. The repository has no release tags, so pin a commit. The changelog gives the commit of each commons release. The crate uses Anchor 1.x and the repository pins Rust 1.94.0. It re-exports anchor_lang, so use dlmm_pro_commons::anchor_lang to keep your Anchor version in step with the crate’s.

Requirements

Cargo does not use the repository’s Cargo.lock for a git dependency, so your own Cargo.lock decides the Anchor, ruint and bytemuck versions. Commit it. To hold Anchor at the version in the repository’s lock file, run cargo update -p anchor-spl --precise 1.1.2, which also moves anchor-lang and the Anchor macro crates. Cargo refuses to downgrade anchor-lang alone while anchor-spl still needs the newer one. ruint and bytemuck are not affected. rev pins the exact commit. Change it only when you move to a newer release of the crate, because the crate’s types follow the IDL of the program release it targets. The crate depends on anchor-lang, anchor-spl, bytemuck, ruint, num_enum, num-integer and spl-transfer-hook-interface. It re-exports only anchor_lang, so a few types in the public signatures need a dependency of your own to name:

What’s in the Crate

Parsing Accounts

Tick arrays, position arrays, limit orders, oracles and pools with extensions have a fixed header followed by a variable tail. The structs from declare_program! describe only the header, so their AccountDeserialize panics on these accounts. Use the Dynamic* parsers instead. Parse the pool first. Whether the pool has the LiquidityMining extension changes the layout of its ticks and positions, so the tick array and position array parsers take the parsed pool:
DynamicPool also exposes the reward slots (pool_reward) and activation rules (pool_activation) when the pool has those extensions. Accounts without a tail, such as TickArrayBitmapExtension and the configs, parse with deserialize_anchor_pod_account.

Quoting a Swap

tick_arrays_for_swap picks the tick arrays holding liquidity in the swap direction, in the order the swap instruction reads them. quote_exact_in then runs the swap over those parsed accounts.
Reading the result: The quote is a snapshot. It does not write the oracle, fee growth, reward growth, activation state or the post-swap volatility, so reparse the accounts before quoting the next swap on the same pool. swapper only matters on pools with the ActivationAccess extension. None is conservative: before activation it fails rather than assume the swapper is whitelisted.

Building the Swap Instruction

Use dlmm_pro::client::accounts::Swap and dlmm_pro::client::args::Swap to build the instruction. The fixed accounts include the pool authority (pool_authority::ID) and the event authority (derive_event_authority()). When pool.is_bitmap_extension_initialized(), pass the tick array bitmap extension too: a swap that searches past the inline bitmap without it fails with BitmapExtensionAccountIsNotProvided. The remaining accounts go in this order:
  1. Transfer hook accounts for each mint that has a hook, as slices described by the remaining_accounts_info argument. A swap accepts the TransferHookX, TransferHookY and TransferHookReferral slices. remaining_accounts_from_hooks builds both the slice list and the account list.
  2. The tick arrays from tick_arrays_for_swap, writable, in the same order you quoted with.
get_extra_account_metas_for_transfer_hook is async. Give it the mint, the mint’s data, a TransferHookContext with the transfer’s source, destination, authority (pool_authority::ID for transfers out of the pool) and amount, and a callback that fetches account data. Mints without a transfer hook need no slice.

Positions and Limit Orders

tick_arrays_for_tick_ids returns the tick arrays holding a set of ticks, sorted by index, which is the order the position and limit order instructions expect. claimable_fees and claimable_rewards preview what a claim would transfer once every slot holding liquidity has been refreshed with refresh_fees_and_rewards. The claim instructions themselves pay out only what is already credited.

Examples

Each example is a complete function. They share one convention: fetch returns the data of an account address, from RPC or your own account cache, and the caller supplies the pool address. Result is anchor_lang::Result.

Fetching and Parsing Accounts

Parse the pool first, because the pool decides how tick arrays and position arrays are laid out. A position array and a limit order live at addresses derived from their NFT mint (derive_position, derive_limit_order), a tick array at derive_tick_array of the pool and its index, and the oracle at derive_oracle of the pool. This example reads one of each account type the crate parses.
DynamicPool::deserialize fails with InvalidAccount when the data is shorter than the extension blocks the pool’s extensions byte announces, and with Anchor’s AccountDiscriminatorMismatch when the bytes belong to another account type. The same two errors apply to the other parsers.

Building a Swap Instruction From a Quote

This function quotes an exact-in swap, resolves the transfer hook accounts of each token leg, and returns a swap instruction with its remaining accounts in the order the program reads them: hook accounts first, then the tick arrays of the quote. It works for mints with and without a transfer hook, because get_extra_account_metas_for_transfer_hook returns an empty list for a mint without one and remaining_accounts_from_hooks writes a slice only for each non-empty list.
Points to check in this example:
  • The direction comes from the mint of user_token_out, as on-chain. The function derives swap_for_y from its argument and expects you to pass token accounts that match it.
  • The transfer out of the pool uses pool_authority::ID as the transfer authority. The transfer in uses the user.
  • The referral hook leg moves the host fee from the reserve of the fee token to the referral account. The fee token is the input mint when fees_on_input is true and the output mint otherwise. When the host fee is zero the program makes no transfer, and the slice is unused.
  • slippage_bps is applied to transfer_fee_excluded_out_amount, the amount the swapper receives after the output mint’s transfer fee. That is the number the program compares with minimum_output_amount.
  • The instruction is not signed or sent. Add your compute budget instructions and the user’s signature.

Reading a Position

get_amounts_from_liquidity turns each range slot’s liquidity into token amounts at the pool’s current price. claimable_fees and claimable_rewards need the ticks at both ends of every slot, so the function fetches the tick arrays with tick_arrays_for_tick_ids and passes all their ticks.
The amounts are what each slot’s total liquidity (unlocked, vesting and permanently locked) is worth at the pool’s current price, rounded down, before any Token 2022 transfer fee. Only the unlocked part can be withdrawn. They do not include pending fees or rewards. The claimable numbers assume every slot holding liquidity is refreshed first. claim_fee and claim_reward themselves pay only what has already been credited to the position, so send refresh_fees_and_rewards before the claim, as the program does not do it for you.

Listing Tick Arrays for a Position or Limit Order

open_position and open_limit_order create the tick arrays they need, so pass every address even when the account does not exist yet. For open_position and for an open_limit_order with absolute tick ids the instruction reads the accounts in ascending index order, which is the order tick_arrays_for_tick_ids returns. A limit order with relative_tick lands on the tick that is current when the transaction runs, so the list must cover every tick array that the accepted band can resolve to.
tick_arrays_to_open_relative_limit_order mirrors the band the TypeScript SDK covers by default. Each entry in offsets is the id of a TickLimitOrderAmount, the number of tick spacings from the spacing-aligned current tick. For a relative order the accounts may be passed in any order, and they may include arrays beyond the ones the final ticks use.

Reading the Oracle for a Time-Weighted Average Tick

The oracle stores a cumulative tick: on each swap the program extends it by the pool’s tick id before the swap, multiplied by the seconds since the last update. The difference between two observations divided by the seconds between them is the average tick over that interval, and get_sqrt_price_from_tick_id turns a tick id into a square root price. The account is a ring buffer, and fixed.idx points at the newest slot.
The stored cumulative tick is as of the observation’s last update, not as of now, and a swap rewrites the newest slot in place until 15 seconds have passed since the slot was created. After that the next swap starts a slot. Slots that no swap has written yet have a zero timestamp, and a brand new oracle holds one empty observation until the first swap. A pool’s oracle starts with one slot, so a time-weighted average needs increase_oracle_length first: until then the function above returns None. increase_oracle_length grows the buffer, up to 500 observations. See Oracle.

Function Reference

Every public item of the crate root is covered here. The step methods on Pool, Tick, DynamicFeeConfig, VariableFee and TickArrayBitmapExtension that quote_exact_in and tick_arrays_for_swap call are public but not listed. Result<T> in a signature is anchor_lang::Result<T>, so errors are anchor_lang::error::Error. A PoolError converts into it, carrying the same code and message the program returns. Amounts are token base units unless a row says otherwise. Prices are Q64.64 square roots: the price of a tick id is 1.0001^tick_id, and a square root price is stored as sqrt(price) * 2^64 in a u128. Fee numerators use FEE_DENOMINATOR (1,000,000,000) as the denominator. Signatures are shown as plain text because they omit bodies. Each one is the crate’s exact signature at the pinned revision.

Account Parsers

All parsers take the raw account data, including the 8-byte discriminator, and return owned values. Header sizes are POOL_INIT_SPACE 640, TICK_ARRAY_INIT_SPACE 56, POSITION_ARRAY_INIT_SPACE 136, LIMIT_ORDER_INIT_SPACE 136 and ORACLE_INIT_SPACE 48. They do not check the account owner, which is your job when you read from RPC, and they do not check that the account belongs to a particular pool. Shared errors: the discriminator of another account type, or data that does not start with the expected one, returns Anchor’s AccountDiscriminatorMismatch (3002). Data shorter than the header, a pool shorter than its extension blocks, or a tail whose length is not a whole number of elements returns InvalidAccount (6038). Bytes after the last expected segment of a pool, or after the header of a fixed account, are ignored.
DynamicPool is Copy. pool is the 640-byte header (IDL Pool). pool_reward is the 352-byte reward block, present when the extensions byte has LiquidityMining (bit 0) set. pool_activation is the 128-byte activation block, present when ActivationAccess (bit 1) is set. The reward block comes first in the account when both are set.
fixed holds pool (the owning pool), tick_index (the array index) and padding. ticks is WithReward (192 bytes per tick) when pool.is_support_liquidity_mining() and Base (160 bytes per tick) otherwise, so pass the owning pool’s header. The tail holds the ticks that have been initialized, at most 128, ordered by tick_id ascending. base_ticks returns a copy of the base Tick of every element, dropping reward growths.
fixed holds pool, nft_mint, delegate_permission, layout_version and vesting. The tail has one element per range slot, 128 bytes (Base) or 192 bytes (WithReward), and the element index is the slot number that bit i of a bitmap selects.
fixed holds pool, nft_mint, referral and delegate_permission. orders has one 24-byte LimitOrderData per tick of the order, with its amount, age, tick_id and side.
fixed.idx is the index of the newest observation and fixed.last_observation_created_timestamp is when that slot was started. observations is the whole ring buffer, 16 bytes per slot. A slot with a zero last_updated_timestamp has never been written (Observation::is_empty).
Parses an account that has no tail: TickArrayBitmapExtension (800 bytes), StaticConfig (120), DynamicConfig (80) and Operator (48). It checks the discriminator of T and reads exactly the size of T after it. Do not use it for pools with extensions, tick arrays, position arrays, limit orders or oracles, because it would read only the header and ignore the tail. AnyBitPattern is bytemuck’s trait and every IDL account type implements it.

Swap Planning and Quotes

Returns the addresses of up to max_tick_arrays tick arrays that hold liquidity, in the order swap traverses them: descending index for swap_for_y, ascending otherwise. The first one is the current tick’s array when it holds liquidity. It returns fewer, down to none, when the bitmap has no more liquidity in that direction. It reads no account data besides the pool header and the extension, so an array that holds only limit orders counts as liquidity. Errors: BitmapExtensionAccountIsNotProvided (6044) when the search leaves the inline bitmap (array indexes -512 to 511) and the pool has an extension but you passed None, plus arithmetic errors. Pass the same arrays, in the same order, as the remaining accounts of swap, after any hook accounts.
Quotes an exact-in swap with the program’s swap loop: the same traversal, the base fee and the dynamic fee, market making liquidity and in-tick limit orders, the fee split and Token 2022 transfer fees. It does not mutate its inputs and does not return the program’s account writes, so parse fresh account data before quoting the next swap on the same pool.
Errors: AmountIsZero for a zero input, InvalidAccount when the extension belongs to another pool or a mint cannot be parsed, InvalidActivationType, InvalidActivationAccess when swaps are disabled or the activation rules block the swapper, LiquidityIsZero when no tick array in the swap direction holds liquidity, InvalidTickArray, BitmapExtensionAccountIsNotProvided, TypeCastFailed for a negative clock timestamp, and MathOverflow. The quote does not write the oracle, fee growth, reward growth, activation state or the post-swap volatility. Scheduled base fees and the dynamic fee move with the clock, so the on-chain result can differ slightly from a quote taken earlier. Set minimum_output_amount below transfer_fee_excluded_out_amount.
The activation check quote_exact_in runs, on its own. It returns InvalidActivationAccess when swaps are disabled in pool_status, or when an activation slot of the ActivationAccess extension still blocks the swap and swapper is not the whitelisted_swap address. current_point is the slot or Unix timestamp that get_current_point returns for the pool’s activation_type.

Positions and Limit Orders

Returns the tick array addresses that hold tick_ids, deduplicated and sorted ascending by array index. pool is the pool address, and tick_spacing is pool.tick_spacing. For a position pass both endpoints of every range, and for a limit order pass the tick id of every tick. It returns an empty list for no ticks and InvalidParameters (6037) for a tick spacing of 0. It does not check that a tick id is usable (inside the tick bounds and a multiple of the spacing), and the program rejects ids that are not.
Returns (amount_x, amount_y) that liquidity represents in the range [tick_lower_id, tick_upper_id) at the current price. Below the range (tick_current_id < tick_lower_id) the amount is all X. At or above the upper tick it is all Y. Inside it, X is priced from the current price to the upper tick and Y from the lower tick to the current price. liquidity is an unscaled integer, such as Position::total_liquidity() for the value a slot holds. tick_current_id and current_sqrt_price come from the pool. round_up is true for what a deposit costs and false for what a withdrawal returns. For example, liquidity 1,000,000,000 over ticks -100 to 100 at tick 0 is 4,987,272 of each token rounded down and 4,987,273 rounded up. Errors: TypeCastFailed when an amount exceeds u64::MAX, and MathOverflow. It ignores fees, rewards, transfer fees and vesting.
Returns (fee_x, fee_y) that claim_fee would transfer once every slot in positions that holds liquidity has been refreshed, summed over the slots. For a slot with liquidity it is fee_pending + liquidity * (fee_growth_inside - checkpoint) >> 64, with the fee growth inside the range taken from pool.fee_growth_global_* and the two end ticks. A slot without liquidity contributes only its stored pending fees. ticks must contain the two end ticks of every slot with liquidity, found by tick_id, otherwise it returns CannotFindTickFromId (6016). Growth counters wrap at 2^128 as on-chain, and sums use checked arithmetic (MathOverflow).
The two steps inside claimable_fees. next_fee_growths_inside returns the Q64.64 fee growth per unit of liquidity accrued inside a tick range: global growth minus the growth below the lower tick minus the growth above the upper tick. An uninitialized tick (liquidity_gross of 0) counts all growth as below it for the lower tick and none as above it for the upper tick. pending_fees returns what a refresh would leave pending on one slot: the stored pending amount plus total_liquidity * (growth_inside - checkpoint) >> 64.
Returns what claim_reward(reward_index) would transfer once every slot holding liquidity is refreshed at current_timestamp, summed over the slots. pool_reward is the reward block of the parsed pool, reward_index is 0 or 1, and current_timestamp is the Unix time in seconds. Rewards accrue to the pool from the slot’s last_update_time to current_timestamp, capped at its end_time, using the pool’s current liquidity. ticks must contain both end ticks of every slot with liquidity. Errors: InvalidRewardIndex (6014) for an index of 2 or more, CannotFindTickFromId, and MathOverflow. Pending amounts per slot saturate at u64::MAX.
The steps inside claimable_rewards. next_rewards_info returns, for each reward slot, the global reward growth after a refresh at current_timestamp and the empty-liquidity seconds that refresh adds to the slot’s counter. A slot that is uninitialized, not yet open, or already finished at its end_time returns its stored growth and 0 seconds. When liquidity is 0 the elapsed seconds are counted as empty-liquidity seconds and growth does not move. next_reward_growths_inside is the reward counterpart of next_fee_growths_inside. pending_rewards returns the pending amount of each slot reward after adding what the growth since the checkpoint earns.

Token 2022

mint_data is the raw mint account, and epoch is the current epoch (Clock::epoch), which selects the older or newer fee schedule of the mint’s TransferFeeConfig. A legacy SPL mint, or a Token 2022 mint without a transfer fee extension, has no fee: get_epoch_transfer_fee returns None and the other two return the amount unchanged with a fee of 0. Mint data that cannot be parsed returns InvalidAccount.
Resolves the accounts that one transfer of mint needs when the mint has a transfer hook. It returns an empty list for a legacy SPL mint and for a Token 2022 mint without a hook, so you can call it for every leg. The result lists the hook’s extra accounts as the hook program declares them, including the hook program and its validation account, with the signer and writable flags from the hook’s account list. The four standard transfer accounts (source, mint, destination, authority) are removed because the program already passes them. Put the list into the TransferHookAccounts field of the leg it belongs to.
TransferHookAccounts implements Default, Clone and Debug. remaining_accounts_from_hooks writes one RemainingAccountsSlice per non-empty list, in the fixed order X, Y, referral, reward, and concatenates the accounts in the same order. It returns the RemainingAccountsInfo argument and the account list. Fill only the lists the target instruction accepts: swap accepts X, Y and referral, add_liquidity and remove_liquidity (and their variants) and claim_fee accept X and Y, claim_reward accepts reward, open_limit_order accepts X for an ask and Y for a bid, and cancel_limit_order accepts X, Y and referral. The program rejects a slice type the instruction does not accept with InvalidRemainingAccountSlice, so the helper does not enforce it for you. A list longer than 255 accounts returns InvalidRemainingAccountSlice. Tick arrays go after these accounts and are not described by a slice. AccountsType, RemainingAccountsInfo and RemainingAccountsSlice are re-exported from the generated types.

Addresses and Constants

Every derive_* helper returns only the address. Use Pubkey::find_program_address with the same seeds when you need the bump. All derive from the DLMM Pro program ID unless the table says otherwise. Seeds are byte strings followed by the listed account keys. Integers are little-endian. ACCESS_REGISTRY_ID is regUFehW9oycnaV2auYyfqyYjKG1W1uLNT7rtUeemQr, the program that owns TokenBadge accounts. The seeds module exposes each prefix as a constant (POOL_PREFIX, TICK_ARRAY_PREFIX, EVENT_AUTHORITY_PREFIX and the rest), and they are also exported at the crate root. dlmm_pro::ID is the program ID, and dlmm_pro::ID_CONST is the same address as a constant. Constants. Most come from the IDL with their IDL types. MIN_TICK_ID, MAX_TICK_ID, MAX_TICKS_PER_ARRAY, NUM_REWARDS, NUMBER_OF_ACTIVATION_MODES and MAX_REFERENCE_AGE are defined by the crate: The *_INIT_SPACE constants are the byte sizes after the discriminator: POOL_INIT_SPACE 640, TICK_ARRAY_INIT_SPACE 56, ORACLE_INIT_SPACE 48, LIMIT_ORDER_INIT_SPACE 136, POSITION_ARRAY_INIT_SPACE 136, TICK_ARRAY_BITMAP_EXTENSION_INIT_SPACE 800, STATIC_CONFIG_INIT_SPACE 120, DYNAMIC_CONFIG_INIT_SPACE 80, OPERATOR_INIT_SPACE 48, TICK_INIT_SPACE 160, TICK_WITH_REWARD_INIT_SPACE 192, POSITION_INIT_SPACE 128, POSITION_WITH_REWARD_INIT_SPACE 192, POOL_REWARD_INIT_SPACE 352, POOL_REWARD_INFO_INIT_SPACE 176, POOL_ACTIVATION_INIT_SPACE 128, POOL_ACTIVATION_INFO_INIT_SPACE 16, LIMIT_ORDER_DATA_INIT_SPACE 24 and OBSERVATION_INIT_SPACE 16.

State Types and Pool Methods

Pool, TickArray, PositionArray, LimitOrder, Oracle, TickArrayBitmapExtension, StaticConfig, DynamicConfig and Operator are the IDL account headers. TokenBadge, BaseFeeConfig, DynamicFeeConfig, FeeConfig, PoolPartner, PositionVesting and VariableFee are IDL types. Their fields are listed on Program Accounts. The crate adds the types that live in the dynamic tails: Enums, all #[repr(u8)] with the numbering the program stores: CollectFeeMode (OutputToken 0, OnlyY 1), PoolType (Static 0, Dynamic 1, LaunchPool 2), PoolExtension (LiquidityMining 0, ActivationAccess 1, a bit position), PoolStatus (Swap 0, AddLiquidity 1, a bit position), TokenProgramFlags (TokenProgram 0, TokenProgram2022 1), PositionDelegatePermission (AddLiquidity 0, RemoveLiquidity 1, ClaimFee 2, ClaimReward 3, UpdatePositionRange 4, bit positions) and LimitOrderDelegatePermission (CancelLimitOrder 0). Each converts to and from u8 with u8::from and try_from. Methods on Pool: Methods on TickArray and TickArrayBitmapExtension: Methods on the tail types: Position::total_liquidity(&self) -> Result<u128> (permanent locked plus unlocked plus vesting), Observation::is_empty(&self) -> bool, LimitOrderData::is_empty(&self) -> bool (no amount and no age), LimitOrderData::is_ask(&self) -> bool, PoolRewardInfo::is_initialized(&self) -> bool (the mint is not the default key) and Tick::is_non_zero_liquidity(&self) -> bool (liquidity, or an open or processing limit order amount). Tick also has the swap step functions get_swap_result_exact_in, get_filled_amount_result_exact_in, apply_swap_result and cross_update, with the result types SwapStepResult and FilledAmountResult. They are the building blocks of quote_exact_in, they operate on one tick at a time, and an integration should call quote_exact_in instead.

Fees, Activation and Math

The base fee numerator of a pool at a point in time, from its fee_config.base_fee (base_fee_mode and 31 bytes of data). current_point is a slot or timestamp that matches the pool’s activation_type, and activation_point, init_sqrt_price and the current sqrt_price come from the pool. BaseFeeMode selects the formula: Fixed returns the cliff fee, the time schedulers (TimeSchedulerLinear, TimeSchedulerExponential) reduce it per period_frequency since activation, and the market cap schedulers (MarketCapSchedulerLinear, MarketCapSchedulerExponential) reduce it per step of price increase above init_sqrt_price. An unknown mode returns InvalidBaseFeeMode. The handler types (PodAlignedFixedFee, PodAlignedFeeTimeScheduler, PodAlignedFeeMarketCapScheduler, the FeeTimeScheduler* and FeeMarketCapScheduler* wrappers and the BaseFeeHandler trait) are the per-mode layouts and formulas behind it. get_fee_in_period(cliff_fee_numerator: u64, reduction_factor: u64, passed_period: u16) -> Result<u64> is the exponential step: cliff * (1 - reduction_factor / 10000)^passed_period.
get_current_point returns clock.slot for ActivationType::Slot (0) and clock.unix_timestamp for ActivationType::Timestamp (1), and InvalidActivationType for any other value. get_activation_access reads one activation slot by its mode (ActivationMode::Open 0, BondingCurve 1, TimeBased 2) and runs f on it, returning InvalidActivationAccess for an unknown mode. The Activation trait has can_swap, can_add_liquidity and can_remove_liquidity, each taking the current point. Open allows everything. BondingCurve allows swaps and allows adding and removing liquidity only once is_reach_target is 1. TimeBased allows each action from its swap_point, add_liquidity_point or remove_liquidity_point. The PodAlignedActivation* structs are the slot layouts. Fee split and curve math. Each returns Result unless its signature shows otherwise: Rounding is Up or Down. The SafeMath trait adds safe_add, safe_sub, safe_mul, safe_div and safe_rem that return PoolError::MathOverflow instead of wrapping, for the integer types and ruint types the crate uses. SafeCast and try_cast convert with PoolError::TypeCastFailed on overflow. down_scale_ceil shifts a ruint value right and rounds up. FeeSetting and SplitFees are the plain structs get_fee_setting and split_fees return.

PoolError

PoolError is the program’s error enum, with the same names, codes and messages as the IDL: 51 variants from MathOverflow (6000) to InvalidLayoutVersion (6050). See Program Errors for the full table. The crate returns these variants:

Program Accounts

Account layouts, PDA seeds and dynamic tails.

Commons README

Crate source, tests and notes.