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#

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();
FieldTypeOffsetDescription
discriminator[u8;8]0Account discriminator ("COALHCST")
versionu88Schema version
market[u8;32]9Market this state belongs to
claim_weight_sumu12841Sum of claim weights (LE)
claim_offset_sumu12857Sum of claim offsets (LE)
bumpu873PDA bump
_padding[u8;14]74Reserved

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#

FunctionDescription
create_initialize_protocol_instructionInitialize protocol config
create_set_fee_config_instructionUpdate fee settings
create_create_market_instructionCreate a new lending market
create_deposit_instructionDeposit tokens to a market
create_borrow_instructionBorrow from a market
create_repay_instructionRepay borrowed amount
create_repay_interest_instructionRepay accrued interest
create_withdraw_instructionWithdraw deposited tokens
create_collect_fees_instructionCollect protocol fees
create_resettle_instructionRe-settle after maturity
create_close_lender_position_instructionClose empty position
create_withdraw_excess_instructionWithdraw excess funds
create_set_borrower_whitelist_instructionManage whitelist
create_set_pause_instructionPause/unpause protocol
create_set_blacklist_mode_instructionConfigure blacklist
create_set_admin_instructionTransfer admin role
create_set_whitelist_manager_instructionSet whitelist manager
create_force_close_position_instructionBorrower force-closes a lender position after grace period
create_claim_haircut_instructionLender claims haircut recovery tokens
create_force_claim_haircut_instructionBorrower 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:

MethodDescription
withdraw_and_closeFull withdrawal + close lender position
claim_haircut_and_closeClaim 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#