Rust SDK#
The Coalesce Finance Rust SDK for building on-chain programs that integrate with the permissioned lending protocol, or for off-chain Rust applications.
Installation#
Add to your Cargo.toml:
[dependencies]
coalescefi-sdk = "0.1"
Feature Flags#
# Default: std environment with all features
coalescefi-sdk = "0.1"
# No-std for on-chain programs (Solana BPF)
coalescefi-sdk = { version = "0.1", default-features = false }
Quick Start — CoalesceClient#
The high-level CoalesceClient handles PDA derivation, ATA resolution, and account
fetching automatically. You only need to provide signers, market addresses, and amounts:
Rust (20 lines)
use coalescefi_sdk::client::CoalesceClient;
use solana_program::pubkey::Pubkey;
let client = CoalesceClient::mainnet("https://api.mainnet-beta.solana.com");
let lender = Pubkey::new_unique();
let market = Pubkey::new_unique();
// Build a deposit instruction (resolves vault, ATA, blacklist check, etc.)
let ixs = client.deposit(&lender, &market, 100_000_000_000, None)?; // 100,000 USDC (6 decimals)
// Withdraw all shares and close the position in one transaction
let ixs = client.withdraw_and_close(&lender, &market, 0, None)?;
// Claim haircut recovery after a settlement shortfall
let ixs = client.claim_haircut(&lender, &market, None)?;
// Claim haircut and close the position in one transaction
let ixs = client.claim_haircut_and_close(&lender, &market, None)?;
Other network constructors: CoalesceClient::devnet(url), CoalesceClient::localnet(url),
or CoalesceClient::new(url, program_id) for a custom program ID.
Quick Start — Low-Level#
Rust (21 lines)
use coalescefi_sdk::{
constants::localnet_program_id,
find_market_pda,
find_lender_position_pda,
accounts::decode_market,
};
use solana_program::pubkey::Pubkey;
// Derive PDAs
let program_id = localnet_program_id();
let borrower = Pubkey::new_unique();
let lender = Pubkey::new_unique();
let market_pda = find_market_pda(&borrower, 1, &program_id);
let lender_pda = find_lender_position_pda(&market_pda.address, &lender, &program_id);
// Parse account data (zero-copy)
let market = decode_market(&account_data)?;
println!("Total deposited: {}", market.total_deposited());
println!("Market bump: {}", market_pda.bump);
println!("Lender position PDA: {}", lender_pda.address);
PDA Derivation#
All PDA functions return PdaResult { address, bump }:
Rust (36 lines)
use coalescefi_sdk::{
constants::localnet_program_id,
find_protocol_config_pda,
find_market_pda,
pdas::find_market_authority_pda,
find_vault_pda,
find_lender_position_pda,
find_borrower_whitelist_pda,
pdas::find_blacklist_check_pda,
pdas::find_haircut_state_pda,
};
use solana_program::pubkey::Pubkey;
let program_id = localnet_program_id();
let borrower = Pubkey::new_unique();
let lender = Pubkey::new_unique();
let blacklist_program = Pubkey::new_unique();
// Protocol config (singleton)
let config_pda = find_protocol_config_pda(&program_id);
// Market PDAs
let market_pda = find_market_pda(&borrower, 1, &program_id);
let authority_pda = find_market_authority_pda(&market_pda.address, &program_id);
let vault_pda = find_vault_pda(&market_pda.address, &program_id);
// Lender position
let lender_pda = find_lender_position_pda(&market_pda.address, &lender, &program_id);
// Borrower whitelist
let whitelist_pda = find_borrower_whitelist_pda(&borrower, &program_id);
let blacklist_check_pda = find_blacklist_check_pda(&lender, &blacklist_program);
// Haircut state (per-market, tracks recovery distribution)
let haircut_state_pda = find_haircut_state_pda(&market_pda.address, &program_id);
Account Parsing#
Zero-Copy Parsing (Recommended for On-Chain)#
The SDK uses bytemuck for zero-copy account parsing:
use coalescefi_sdk::{accounts::decode_market, Market};
use solana_program::pubkey::Pubkey;
// Zero-copy parse (no allocation)
let market: &Market = decode_market(&account_data)?;
// Access fields directly
let borrower: Pubkey = market.borrower_pubkey();
let interest_rate: u16 = market.annual_interest_bps();
let maturity: i64 = market.maturity_timestamp();
let total_deposited: u64 = market.total_deposited();
Account Discriminators#
Validate account types using discriminators:
use coalescefi_sdk::constants::{
DISC_PROTOCOL_CONFIG,
DISC_MARKET,
DISC_LENDER_POSITION,
DISC_BORROWER_WL,
};
use solana_program::program_error::ProgramError;
fn validate_market_account(data: &[u8]) -> Result<(), ProgramError> {
if data.len() < 8 || &data[..8] != DISC_MARKET {
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
HaircutState Account#
The HaircutState account (88 bytes) tracks per-market haircut recovery distribution
state. It is created alongside the market and updated during settlement and claims.
use coalescefi_sdk::{accounts::decode_haircut_state, HaircutState};
let haircut_state: &HaircutState = decode_haircut_state(&account_data)?;
let market: Pubkey = haircut_state.market_pubkey();
let claim_weight_sum: u128 = haircut_state.claim_weight_sum();
let claim_offset_sum: u128 = haircut_state.claim_offset_sum();
| Field | Type | Offset | Description |
|---|---|---|---|
discriminator | [u8;8] | 0 | Account discriminator ("COALHCST") |
version | u8 | 8 | Schema version |
market | [u8;32] | 9 | Market this state belongs to |
claim_weight_sum | u128 | 41 | Sum of claim weights (LE) |
claim_offset_sum | u128 | 57 | Sum of claim offsets (LE) |
bump | u8 | 73 | PDA bump |
_padding | [u8;14] | 74 | Reserved |
Instruction Building#
Build Instructions#
Rust (28 lines)
use coalescefi_sdk::{
constants::{localnet_program_id, spl_token_program_id, system_program_id},
instructions::{create_deposit_instruction, DepositAccounts},
types::DepositArgs,
};
use solana_program::pubkey::Pubkey;
let program_id = localnet_program_id();
let accounts = DepositAccounts {
market: Pubkey::new_unique(),
lender: Pubkey::new_unique(),
lender_token_account: Pubkey::new_unique(),
vault: Pubkey::new_unique(),
lender_position: Pubkey::new_unique(),
blacklist_check: Pubkey::new_unique(),
protocol_config: Pubkey::new_unique(),
mint: Pubkey::new_unique(),
token_program: spl_token_program_id(),
system_program: system_program_id(),
};
let args = DepositArgs {
amount: 1_000_000, // 1 USDC
};
let ix = create_deposit_instruction(accounts, args, &program_id);
Available Instructions#
| Function | Description |
|---|---|
create_initialize_protocol_instruction | Initialize protocol config |
create_set_fee_config_instruction | Update fee settings |
create_create_market_instruction | Create a new lending market |
create_deposit_instruction | Deposit tokens to a market |
create_borrow_instruction | Borrow from a market |
create_repay_instruction | Repay borrowed amount |
create_repay_interest_instruction | Repay accrued interest |
create_withdraw_instruction | Withdraw deposited tokens |
create_collect_fees_instruction | Collect protocol fees |
create_resettle_instruction | Re-settle after maturity |
create_close_lender_position_instruction | Close empty position |
create_withdraw_excess_instruction | Withdraw excess funds |
create_set_borrower_whitelist_instruction | Manage whitelist |
create_set_pause_instruction | Pause/unpause protocol |
create_set_blacklist_mode_instruction | Configure blacklist |
create_set_admin_instruction | Transfer admin role |
create_set_whitelist_manager_instruction | Set whitelist manager |
create_force_close_position_instruction | Borrower force-closes a lender position after grace period |
create_claim_haircut_instruction | Lender claims haircut recovery tokens |
create_force_claim_haircut_instruction | Borrower force-claims haircut recovery on behalf of a lender |
Combined Builders (CoalesceClient)#
The CoalesceClient also provides combined builders that bundle multiple instructions
into a single transaction:
| Method | Description |
|---|---|
withdraw_and_close | Full withdrawal + close lender position |
claim_haircut_and_close | Claim haircut recovery + close lender position |
Error Handling#
Rust (18 lines)
use coalescefi_sdk::errors::CoalescefiError;
match result {
Err(CoalescefiError::InsufficientBalance) => {
msg!("Not enough funds in account");
}
Err(CoalescefiError::Unauthorized) => {
msg!("Signer not authorized");
}
Err(CoalescefiError::NotMatured) => {
msg!("Market hasn't reached maturity");
}
Err(e) => {
msg!("Other error: {:?}", e);
}
Ok(_) => {}
}
Error Codes#
Rust (61 lines)
use coalescefi_sdk::errors::CoalescefiError;
// Initialization errors (0-4)
CoalescefiError::AlreadyInitialized // 0
CoalescefiError::InvalidFeeRate // 1
CoalescefiError::InvalidCapacity // 2
CoalescefiError::InvalidMaturity // 3
CoalescefiError::MarketAlreadyExists // 4
// Authorization errors (5-9)
CoalescefiError::Unauthorized // 5
CoalescefiError::NotWhitelisted // 6
CoalescefiError::Blacklisted // 7
CoalescefiError::ProtocolPaused // 8
CoalescefiError::BorrowerHasActiveDebt // 9
// Account validation errors (10-16)
CoalescefiError::InvalidAddress // 10
CoalescefiError::InvalidMint // 11
CoalescefiError::InvalidVault // 12
CoalescefiError::InvalidPDA // 13
CoalescefiError::InvalidAccountOwner // 14
CoalescefiError::InvalidTokenProgram // 15
CoalescefiError::InvalidTokenAccountOwner // 16
// Input validation errors (17-20)
CoalescefiError::ZeroAmount // 17
CoalescefiError::ZeroScaledAmount // 18
CoalescefiError::InvalidScaleFactor // 19
CoalescefiError::InvalidTimestamp // 20
// Balance/capacity errors (21-27)
CoalescefiError::InsufficientBalance // 21
CoalescefiError::InsufficientScaledBalance // 22
CoalescefiError::NoBalance // 23
CoalescefiError::ZeroPayout // 24
CoalescefiError::CapExceeded // 25
CoalescefiError::BorrowAmountTooHigh // 26
CoalescefiError::GlobalCapacityExceeded // 27
// Market state errors (28-35)
CoalescefiError::MarketMatured // 28
CoalescefiError::NotMatured // 29
CoalescefiError::NotSettled // 30
CoalescefiError::SettlementNotImproved // 31
CoalescefiError::SettlementGracePeriod // 32
CoalescefiError::SettlementNotComplete // 33
CoalescefiError::PositionNotEmpty // 34
CoalescefiError::RepaymentExceedsDebt // 35
// Fee/withdrawal errors (36-40)
CoalescefiError::NoFeesToCollect // 36
CoalescefiError::FeeCollectionDuringDistress // 37
CoalescefiError::LendersPendingWithdrawals // 38
CoalescefiError::FeesNotCollected // 39
CoalescefiError::NoExcessToWithdraw // 40
// Operational errors (41-42)
CoalescefiError::MathOverflow // 41
CoalescefiError::PayoutBelowMinimum // 42
Constants#
Rust (29 lines)
use coalescefi_sdk::constants::{
// Mathematical constants
WAD, // 10^18
BPS, // 10000
SECONDS_PER_YEAR, // 31_536_000
// Protocol limits
MAX_ANNUAL_INTEREST_BPS, // 10000 (100%)
MAX_FEE_RATE_BPS, // 10000 (100%)
USDC_DECIMALS, // 6
// Account sizes
PROTOCOL_CONFIG_SIZE, // 194 bytes
MARKET_SIZE, // 250 bytes
LENDER_POSITION_SIZE, // 128 bytes
BORROWER_WHITELIST_SIZE, // 96 bytes
HAIRCUT_STATE_SIZE, // 88 bytes
// Seeds
SEED_PROTOCOL_CONFIG,
SEED_MARKET,
SEED_MARKET_AUTHORITY,
SEED_VAULT,
SEED_LENDER,
SEED_BORROWER_WHITELIST,
SEED_BLACKLIST,
SEED_HAIRCUT_STATE,
};
Math Reference#
The math module provides WAD-precision (10^18) fixed-point arithmetic matching the on-chain Rust implementation exactly.
Rust (32 lines)
use coalescefi_sdk::math::{
// Core WAD arithmetic
mul_wad, // (a * b) / WAD
pow_wad, // base^exp in WAD
growth_factor_wad, // Daily compound + linear sub-day growth
// Deposit/Withdraw conversions
calculate_scaled_amount, // tokens → sTokens (shares)
calculate_normalized_amount, // sTokens → tokens
// Position analysis
calculate_position_value, // Current value of lender shares
estimate_value_at_maturity, // Projected value at maturity
calculate_settlement_payout, // Actual payout after settlement
// Market analytics
estimate_interest_accrual, // Preview interest growth
calculate_total_supply, // Normalized total supply
calculate_available_vault_balance, // Deprecated approximation; prefer vault RPC balance
calculate_utilization_rate, // Outstanding principal / supply (bps + decimal)
calculate_apr, // BPS to decimal (e.g., 1000 → 0.10)
// Utilities
safe_divide, // Returns 0 on division by zero
would_overflow_u64,
would_overflow_u128,
// Constants
SECONDS_PER_DAY, // 86,400
DAYS_PER_YEAR, // 365
};
Deposit Preview#
use coalescefi_sdk::math::calculate_scaled_amount;
use coalescefi_sdk::WAD;
// How many shares does depositing 1 USDC yield?
let scale_factor = WAD * 11 / 10; // 1.10 (10% interest accrued)
let shares = calculate_scaled_amount(1_000_000, scale_factor).unwrap();
// shares < 1_000_000 (fewer shares because each is worth more)
Interest Accrual Preview#
use coalescefi_sdk::math::estimate_interest_accrual;
use coalescefi_sdk::accounts::decode_market;
let market = decode_market(&account_data).unwrap();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).unwrap()
.as_secs() as i64;
let result = estimate_interest_accrual(market, now).unwrap();
println!("Estimated scale factor: {}", result.new_scale_factor);
println!("Interest delta: {}", result.interest_delta);
Market Analytics#
For exact available liquidity, read the market vault SPL Token account balance via RPC.
calculate_available_vault_balance is a deprecated counter-based approximation and can
diverge after fee collection, withdraw_excess, force_close_position, or withdrawal payouts.
Rust (21 lines)
use coalescefi_sdk::math::{
calculate_apr, calculate_total_supply, calculate_utilization_rate,
};
use coalescefi_sdk::accounts::decode_market;
let market = decode_market(&account_data).unwrap();
let apr = calculate_apr(market.annual_interest_bps());
let total_supply = calculate_total_supply(
market.scaled_total_supply(), market.scale_factor()
).unwrap();
let utilization = calculate_utilization_rate(market).unwrap();
println!("APR: {:.2}%", apr * 100.0);
println!("Total supply: {}", total_supply);
println!("Utilization: {:.2}% ({} bps)", utilization.decimal * 100.0, utilization.bps);
// Exact available vault balance: read the SPL Token account balance for market.vault via RPC.
// Deprecated approximation if you only have market counters:
// let approx_available = calculate_available_vault_balance(market);
No-Std Support#
For on-chain programs, use the SDK without std:
[dependencies]
coalescefi-sdk = { version = "0.1", default-features = false }
#![no_std]
use coalescefi_sdk::find_market_pda;
// All core functionality works without std
For account layouts and byte offsets, see the architecture documentation.
Integration Example#
Complete instruction-construction example:
Rust (24 lines)
use coalescefi_sdk::{
constants::{localnet_program_id, spl_token_program_id, system_program_id},
instructions::{create_deposit_instruction, DepositAccounts},
types::DepositArgs,
};
use solana_program::pubkey::Pubkey;
let program_id = localnet_program_id();
let accounts = DepositAccounts {
market: Pubkey::new_unique(),
lender: Pubkey::new_unique(),
lender_token_account: Pubkey::new_unique(),
vault: Pubkey::new_unique(),
lender_position: Pubkey::new_unique(),
blacklist_check: Pubkey::new_unique(),
protocol_config: Pubkey::new_unique(),
mint: Pubkey::new_unique(),
token_program: spl_token_program_id(),
system_program: system_program_id(),
};
let ix = create_deposit_instruction(accounts, DepositArgs { amount: 1_000_000 }, &program_id);
Testing#
Rust (22 lines)
#[cfg(test)]
mod tests {
use super::*;
use coalescefi_sdk::constants::*;
#[test]
fn test_pda_derivation() {
let program_id = localnet_program_id();
let borrower = Pubkey::new_unique();
let market = find_market_pda(&borrower, 1, &program_id);
assert!(market.bump > 0);
assert_ne!(market.address, Pubkey::default());
}
#[test]
fn test_account_size() {
assert_eq!(std::mem::size_of::<Market>(), MARKET_SIZE);
}
}
Next Steps#
- TypeScript SDK - For frontend applications
- Python SDK - For scripts and backend
- Error Codes - Error codes reference