LODE / Carpenter SDK

Carpenter SDK

Getting started

Install

Package
@lodefoundation/carpenter-sdk
Peer dependency
@solana/kit 8.3.0
Release
0.1.0
Quoter wasm
Shipped in the package. An install needs no Rust toolchain.
Rust
Git dependencies at the release tag. Not on crates.io.
Licence
Apache-2.0
npm install @lodefoundation/carpenter-sdk @solana/kit@8.3.0

Clusters

Clustercarpenter_ammamm_config
mainnet-betaDdeiDx4ncHo9DXGZm1GrPe7mFKv7kNU3yx95uQEV2APA85S8wiRLyv9Urc6g9tF8gDC56F5Fq4JBu9Zub3BEtyuV
devnetArv1M4LifBrWMywvxpeAGvh37aVHPjpXPCg7sacbe3Hy4MQRpJ85JE6FqBWFj47GcCoJpdFMVsLsMVPAq9865iiE

Program ids come from deploymentFor, never from a constant in an app. DEFAULT_PROGRAM_IDS is mainnet’s, for tests and scripts.

TypeScript

import { createSolanaRpc } from '@solana/kit';
import { deploymentFor, fromKitRpc, MAINNET_CHAIN_KEY, programIdsFromDeployment } from '@lodefoundation/carpenter-sdk';

const rpc = createSolanaRpc(process.env.SOLANA_RPC_URL);
const src = fromKitRpc(rpc);
// DEVNET_CHAIN_KEY for devnet
const ids = programIdsFromDeployment(deploymentFor(MAINNET_CHAIN_KEY));

Pools

Read a pool

Pools are keyed by (mint0, mint1, fee, tickSpacing, hook). Every stateView read answers { slot, value }.

import { stateView } from '@lodefoundation/carpenter-sdk';

// every read is tagged with the slot it was read at
const { slot, value: pool } = await stateView.getPool(src, poolAddress, ids);

Quote and swap

The quoter is the program’s own quoting crate compiled to wasm. It quotes pools with no swap hook.

PoolAnswer
No hook, or a hook with no swap flagQuoted
A hook that takes part in swapsQuoteHookNotQuotable: quoted by its hook’s SDK (@lodefoundation/sdk-solana for LODE pools)
PausedPoolPaused
amountSpecified
Negative is exact-in, positive exact-out.
amountLimit
The minimum out, required. 0 is refused unless allowNoMinimum: true.
Compute
CU_LIMITS are estimates; set the limit from a simulation’s unitsConsumed.
Native SOL
A wSOL side goes through a temporary wSOL account the builder creates, funds and closes in the same transaction.
import { buildSwap, compileSendable, decodePool, readSwapQuoteAccounts, swapTickArrays } from '@lodefoundation/carpenter-sdk';
import { initQuote } from '@lodefoundation/carpenter-sdk/quote/wasm';

const quoter = await initQuote(wasmBytes); // carpenter_quote_bg.wasm, shipped in the package
const { value: v } = await readSwapQuoteAccounts(src, poolAddress, { radius: 4, ids });
const q = quoter.quoteSwap({ ...v, zeroForOne: true, amountSpecified: -1_000_000n }); // exact in: negative
const { starts } = await swapTickArrays(poolAddress, q, ids);

const tx = await buildSwap({
  trader, pool: poolAddress, poolState: decodePool(v.pool), zeroForOne: true,
  amountSpecified: -1_000_000n, amountLimit: (q.amountOut * 99n) / 100n,
  tickArrayStarts: starts, ids,
});
const { value: bh } = await rpc.getLatestBlockhash().send();
const { transaction } = compileSendable('swap', { feePayer: trader, instructions: tx.instructions, lifetime: bh });

Liquidity

buildModifyLiquidity adds and removes liquidity and collects fees. On a launch with holder rewards, pass @lodefoundation/sdk-solana’s transferHookAccounts.

Removal
Refused with neither amount0Min nor amount1Min.
Deposit
Refused when its maximum is u64::MAX.
Opt-out
allowNoMinimum: true

Create a pool

planCreatePool answers two transactions, [initializePool, addLiquidity], signed with one prompt and sent in order. The position’s owner is the payer.

Between the steps
The pool exists and is empty; anyone can move an empty pool’s price.
beforeSend
Reads the pool before the deposit and stops the run (PoolPriceMovedError) when its price moved more than maxPriceMoveBps (default 0).
Hard bound
amountAMax / amountBMax.

TypeScript

import { fromKitRpc, planCreatePool, sendSolanaTransactions, NO_HOOK } from '@lodefoundation/carpenter-sdk';

const plan = await planCreatePool({
  payer, mintA, mintB, tokenProgramA, tokenProgramB, decimalsA: 6, decimalsB: 9,
  fee: 3_000, tickSpacing: 60, hook: NO_HOOK, sqrtPriceX64,
  position: { tickLower: -600, tickUpper: 600, liquidity, amountAMax, amountBMax },
  lifetime: bh, ids,
});
await sendSolanaTransactions(wallet, plan.transactions, rpc, {
  steps: plan.stepNames,
  lastValidBlockHeight: bh.lastValidBlockHeight,
  // stop if the new pool's price moved before the deposit
  beforeSend: plan.beforeSend(fromKitRpc(rpc)),
});

Send

ActionTransactionsPrompts
Swap11
Add or remove liquidity, collect fees11
Create a pool with its first position21
Signing
solana:signTransaction with every transaction, else signAllTransactions, else one prompt per transaction.
Failure
SolanaSendError: index, step, reason, the signatures that landed.
Blockhash
Shared, valid for about 60–90 s. opts.replan rebuilds what has not landed; without it the run stops with expired.
A step
Never sent twice.

TypeScript

import { getBase64EncodedWireTransaction } from '@solana/kit';
import { sendSolanaTransactions } from '@lodefoundation/carpenter-sdk';

await sendSolanaTransactions(
  wallet,
  [getBase64EncodedWireTransaction(transaction)],
  rpc,
  { lastValidBlockHeight: bh.lastValidBlockHeight },
);

Rust

Crates

CrateWhat
carpenter-typesAccount layouts, PDA seeds, constants, discriminators, the program id, the hook interface
carpenter-clmm-mathQ64.64 tick, sqrt-price and liquidity math, the swap step, the tick-array bitmap
carpenter-quotequote_swap for plain and hooked pools, the HookQuoter interface, liquidity and fee quotes
carpenter-ammA jupiter-amm-interface Amm over the quoters (its own workspace, jupiter/)

No crate depends on a Solana crate but carpenter-amm: keys are [u8; 32], and the same bytes give the same answer on every target, wasm32 included.

Rust

# Cargo.toml
[dependencies]
carpenter-quote = { git = "https://github.com/gudtekdonkey/carpenter-sdk", tag = "0.1.0" }
carpenter-types = { git = "https://github.com/gudtekdonkey/carpenter-sdk", tag = "0.1.0" }

HookQuoter

An approved hook that changes swaps ships a Rust replay of its callbacks implementing HookQuoter. LODE’s is FloorHookQuoter in floor-quote.

PoolResult
No hook, or a hook with no swap flagThe plain walk
A swap hook with a quoter that accepts itThe round protocol through that quoter
A swap hook with no quoterQuoteHookNotQuotable, never quoted as plain

Rust

pub trait HookQuoter {
    fn program_id(&self) -> [u8; 32];
    fn accepts(&self, hook: &[u8; 32]) -> bool { self.program_id() == *hook }
    fn prepare(&mut self, pool: &PoolAccount) -> QuoteResult<()> { Ok(()) }
    fn before_swap(&mut self, ctx: &HookSwapCtx) -> QuoteResult<HookRecordV1>;
    fn after_swap(&mut self, ctx: &HookSwapCtx, after: &AfterSwapIn) -> QuoteResult<HookRecordV1>;
    fn after_actions(&mut self, ctx: &HookSwapCtx, round: u8, results: &[ActionResult],
                     results_carry_fees: bool, claims0: u64, claims1: u64) -> QuoteResult<HookRecordV1>;
}

// quote_swap(pool, arrays, Some(&mut quoter), params)
// quote_swap_with(pool, arrays, &mut [..quoters], params)

Jupiter adapter

carpenter-amm quotes a pool from account bytes and lists the accounts of carpenter_amm’s swap. LODE pools are quoted through floor-quote; a pool whose hook it has no quoter for is refused.

Rust

# carpenter-sdk/jupiter: its own Cargo workspace
cd jupiter && cargo test

# label "Carpenter": plain pools, and LODE pools through floor-quote
# get_swap_and_account_metas returns Swap::Placeholder until Jupiter's
# interface carries a Carpenter variant

Reference

LODE SDK

Reading LODE launches, quoting them and building their trades, on Ethereum and EVM: the SDK page.