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
# 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" }npm install @lodefoundation/carpenter-sdk @solana/kit@8.3.0
Clusters
| Cluster | carpenter_amm | amm_config |
|---|---|---|
mainnet-beta | DdeiDx4ncHo9DXGZm1GrPe7mFKv7kNU3yx95uQEV2APA | 85S8wiRLyv9Urc6g9tF8gDC56F5Fq4JBu9Zub3BEtyuV |
devnet | Arv1M4LifBrWMywvxpeAGvh37aVHPjpXPCg7sacbe3Hy | 4MQRpJ85JE6FqBWFj47GcCoJpdFMVsLsMVPAq9865iiE |
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);use carpenter_quote::state::decode_pool; let pool = decode_pool(&pool_account_bytes)?;
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.
| Pool | Answer |
|---|---|
| No hook, or a hook with no swap flag | Quoted |
| A hook that takes part in swaps | QuoteHookNotQuotable: quoted by its hook’s SDK (@lodefoundation/sdk-solana for LODE pools) |
| Paused | PoolPaused |
- amountSpecified
- Negative is exact-in, positive exact-out.
- amountLimit
- The minimum out, required.
0is refused unlessallowNoMinimum: true. - Compute
CU_LIMITSare estimates; set the limit from a simulation’sunitsConsumed.- 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 });use carpenter_quote::state::{decode_pool, TickArraySet};
use carpenter_quote::{no_limit, quote_plain, PoolAccount, SwapQuoteParams};
let pool = decode_pool(&pool_account_bytes)?;
let set = TickArraySet::from_accounts(&pool_key, pool.tick_spacing, &tick_array_accounts, Some(&bitmap_account))?;
let q = quote_plain(
&PoolAccount { key: pool_key, pool },
&set,
&SwapQuoteParams { zero_for_one: true, amount_specified: -1_000_000, sqrt_price_limit_x64: no_limit(true), supply0, supply1 },
)?;
// q.amount_in, q.amount_out, q.fees, q.all_arrays()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
amount0Minnoramount1Min. - 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 thanmaxPriceMoveBps(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
| Action | Transactions | Prompts |
|---|---|---|
| Swap | 1 | 1 |
| Add or remove liquidity, collect fees | 1 | 1 |
| Create a pool with its first position | 2 | 1 |
- Signing
solana:signTransactionwith every transaction, elsesignAllTransactions, else one prompt per transaction.- Failure
SolanaSendError:index,step,reason, the signatures that landed.- Blockhash
- Shared, valid for about 60–90 s.
opts.replanrebuilds what has not landed; without it the run stops withexpired. - 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
| Crate | What |
|---|---|
carpenter-types | Account layouts, PDA seeds, constants, discriminators, the program id, the hook interface |
carpenter-clmm-math | Q64.64 tick, sqrt-price and liquidity math, the swap step, the tick-array bitmap |
carpenter-quote | quote_swap for plain and hooked pools, the HookQuoter interface, liquidity and fee quotes |
carpenter-amm | A 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.
| Pool | Result |
|---|---|
| No hook, or a hook with no swap flag | The plain walk |
| A swap hook with a quoter that accepts it | The round protocol through that quoter |
| A swap hook with no quoter | QuoteHookNotQuotable, 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.