TypeScript SDK

The TypeScript SDK (@coalescefi/sdk) for building web apps and Node.js backends on the Coalesce Finance permissioned lending protocol.

Installation

Package Manager

npm install @coalescefi/sdk

For Squads multisig integration:

npm install @coalescefi/sdk @solana/web3.js @sqds/multisig

Peer Dependencies

The SDK currently declares this peer dependency:

{
  "peerDependencies": {
    "@solana/web3.js": "^1.91.0"
  }
}

Most instruction-building integrations also use SPL token helpers, so install both in app projects:

npm install @solana/web3.js @solana/spl-token

Shared Core

The SDK depends on @coalescefi/shared-core for calculations:

npm install @coalescefi/shared-core

This is typically installed automatically as a dependency.

Note: The SDK requires TypeScript target ES2020 or later for BigInt support.

Verifying Installation

import { findMarketPda, findProtocolConfigPda } from '@coalescefi/sdk';
import { PublicKey } from '@solana/web3.js';

// Test PDA derivation
const programId = new PublicKey('CoaLfi...');
const [configPda] = findProtocolConfigPda(programId);

console.log('SDK installed successfully');
console.log('Config PDA:', configPda.toBase58());

Version Compatibility

SDK VersionSolana/web3.jsProgram Version
0.1.x^1.91.0v1

Updating

# Check current version
npm list @coalescefi/sdk

# Update to latest
npm update @coalescefi/sdk

# Update to specific version
npm install @coalescefi/sdk@0.1.0

Configuration

Program ID

The SDK needs to know the Coalesce Finance program ID:

Mainnet

import { PublicKey } from '@solana/web3.js';

const MAINNET_PROGRAM_ID = new PublicKey('CoaL...'); // Replace with actual

Devnet

const DEVNET_PROGRAM_ID = new PublicKey('CoaL...'); // Replace with actual

Passing Program ID

All SDK functions accept programId as a parameter:

import { findMarketPda } from '@coalescefi/sdk';

const [marketPda] = findMarketPda(borrower, nonce, PROGRAM_ID);

Connection Setup

Standard Connection

import { Connection, clusterApiUrl } from '@solana/web3.js';

// Devnet
const connection = new Connection(clusterApiUrl('devnet'));

// Mainnet
const connection = new Connection(clusterApiUrl('mainnet-beta'));

// Custom RPC
const connection = new Connection('https://your-rpc-endpoint.com');

With Options

const connection = new Connection('https://your-rpc.com', {
  commitment: 'confirmed',
  confirmTransactionInitialTimeout: 60000,
});

USDC Mint

Configure the USDC token mint:

Mainnet USDC

const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');

Devnet USDC

const DEVNET_USDC_MINT = new PublicKey('...'); // Devnet test USDC

Environment Configuration

Using Environment Variables

// config.ts
import { PublicKey } from '@solana/web3.js';

export const config = {
  programId: new PublicKey(process.env.COALESCEFI_PROGRAM_ID!),
  usdcMint: new PublicKey(process.env.USDC_MINT!),
  rpcUrl: process.env.SOLANA_RPC_URL!,
};

.env File

COALESCEFI_PROGRAM_ID=CoaL...
USDC_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
SOLANA_RPC_URL=https://your-rpc.com

Environment-Based Config

const isMainnet = process.env.NETWORK === 'mainnet';

export const config = {
  programId: isMainnet ? MAINNET_PROGRAM_ID : DEVNET_PROGRAM_ID,
  usdcMint: isMainnet ? MAINNET_USDC : DEVNET_USDC,
  rpcUrl: isMainnet ? 'https://mainnet-rpc.com' : 'https://api.devnet.solana.com',
};

SDK Configuration Object

Create a reusable configuration:

// coalesceConfig.ts
import { Connection, PublicKey } from '@solana/web3.js';

export interface CoalesceConfig {
  connection: Connection;
  programId: PublicKey;
  usdcMint: PublicKey;
}

export function createConfig(network: 'mainnet' | 'devnet'): CoalesceConfig {
  if (network === 'mainnet') {
    return {
      connection: new Connection('https://your-mainnet-rpc.com'),
      programId: new PublicKey('CoaL...'),
      usdcMint: new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
    };
  } else {
    return {
      connection: new Connection('https://api.devnet.solana.com'),
      programId: new PublicKey('CoaL...'),
      usdcMint: new PublicKey('...'),
    };
  }
}

// Usage
const config = createConfig('mainnet');

Wallet Configuration

With Wallet Adapter (Browser)

import { useWallet, useConnection } from '@solana/wallet-adapter-react';

function MyComponent() {
  const { connection } = useConnection();
  const { publicKey, signTransaction } = useWallet();

  // Use publicKey and signTransaction with SDK
}

With Keypair (Server/Script)

import { Keypair } from '@solana/web3.js';
import bs58 from 'bs58';

// From file
const keypair = Keypair.fromSecretKey(Uint8Array.from(require('./keypair.json')));

// From base58 string
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY!));

Commitment Levels

Setting Commitment

// Per-request
const accountInfo = await connection.getAccountInfo(address, 'confirmed');

// Default commitment
const connection = new Connection(rpcUrl, 'confirmed');
Use CaseCommitment
Reading market dataconfirmed
Checking balancesconfirmed
After transactionconfirmed
Critical operationsfinalized

Timeout Configuration

Transaction Timeout

import { sendAndConfirmTransaction } from '@solana/web3.js';

const signature = await sendAndConfirmTransaction(connection, transaction, [signer], {
  commitment: 'confirmed',
  maxRetries: 3,
});

Quick Start

import { Connection, PublicKey, Keypair, Transaction, SystemProgram } from '@solana/web3.js';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import {
  findMarketPda,
  findLenderPositionPda,
  findVaultPda,
  findProtocolConfigPda,
  findBlacklistCheckPda,
  createDepositInstruction,
  decodeMarket,
} from '@coalescefi/sdk';

// Setup
const connection = new Connection('https://api.devnet.solana.com');
const programId = new PublicKey('YOUR_PROGRAM_ID');
const borrower = new PublicKey('BORROWER_PUBKEY');
const lender = yourWallet.publicKey;
const usdcMint = new PublicKey('USDC_MINT');
const blacklistProgram = new PublicKey('BLACKLIST_PROGRAM');
const marketNonce = 1n;

// 1. Derive PDAs
const [marketPda] = findMarketPda(borrower, marketNonce, programId);
const [vaultPda] = findVaultPda(marketPda, programId);
const [positionPda] = findLenderPositionPda(marketPda, lender, programId);
const [configPda] = findProtocolConfigPda(programId);
const [blacklistCheckPda] = findBlacklistCheckPda(lender, blacklistProgram);

// 2. Read market data
const marketAccount = await connection.getAccountInfo(marketPda);
const market = decodeMarket(marketAccount!.data);

console.log('Market Interest Rate:', market.annualInterestBps / 100, '%');

// 3. Build deposit instruction
const depositIx = createDepositInstruction(
  {
    market: marketPda,
    lender: lender,
    lenderTokenAccount: lenderUsdcAccount,
    vault: vaultPda,
    lenderPosition: positionPda,
    blacklistCheck: blacklistCheckPda,
    protocolConfig: configPda,
    mint: usdcMint,
    tokenProgram: TOKEN_PROGRAM_ID,
    systemProgram: SystemProgram.programId,
  },
  { amount: 1_000_000n } // 1 USDC
);

// 4. Send transaction
const tx = new Transaction().add(depositIx);
const signature = await sendAndConfirmTransaction(connection, tx, [yourWallet]);

PDA Derivation Reference

All account addresses in Coalesce Finance are Program Derived Addresses (PDAs). The SDK provides helper functions for deriving each PDA type. See the PDA Reference for the full seeds table.

Protocol Config PDA

The singleton protocol configuration account:

import { findProtocolConfigPda } from '@coalescefi/sdk';

const [configPda, configBump] = findProtocolConfigPda(programId);
// Seeds: ["protocol_config"]

Market PDA

Each market is uniquely identified by borrower + nonce:

import { findMarketPda } from '@coalescefi/sdk';

const borrower = new PublicKey('BORROWER_WALLET');
const marketNonce = 1n; // bigint, increments per market

const [marketPda, marketBump] = findMarketPda(borrower, marketNonce, programId);
// Seeds: ["market", borrower, nonce_le_bytes]

Market Authority PDA

The authority that signs vault token transfers:

import { findMarketAuthorityPda } from '@coalescefi/sdk';

const [authorityPda, authorityBump] = findMarketAuthorityPda(marketPda, programId);
// Seeds: ["market_authority", market]

Vault PDA

The token account holding market deposits:

import { findVaultPda } from '@coalescefi/sdk';

const [vaultPda, vaultBump] = findVaultPda(marketPda, programId);
// Seeds: ["vault", market]

Lender Position PDA

Each lender's position in a specific market:

import { findLenderPositionPda } from '@coalescefi/sdk';

const [positionPda, positionBump] = findLenderPositionPda(marketPda, lenderWallet, programId);
// Seeds: ["lender", market, lender]

Borrower Whitelist PDA

A borrower's whitelisted status and capacity:

import { findBorrowerWhitelistPda } from '@coalescefi/sdk';

const [whitelistPda, whitelistBump] = findBorrowerWhitelistPda(borrowerWallet, programId);
// Seeds: ["borrower_whitelist", borrower]

Derive All Market PDAs at Once

Convenience function for market-related PDAs:

import { deriveMarketPdas } from '@coalescefi/sdk';

const { market, marketAuthority, vault } = deriveMarketPdas(borrowerWallet, marketNonce, programId);

Complete Operation Examples

1. Create Market (Borrower)

Borrowers create markets to attract lender deposits.

import {
  Connection,
  Keypair,
  PublicKey,
  SystemProgram,
  Transaction,
  sendAndConfirmTransaction,
} from '@solana/web3.js';
import {
  findProtocolConfigPda,
  findMarketPda,
  findMarketAuthorityPda,
  findVaultPda,
  findBorrowerWhitelistPda,
  findBlacklistCheckPda,
  createCreateMarketInstruction,
  decodeBorrowerWhitelist,
  decodeProtocolConfig,
  configFieldToPublicKey,
} from '@coalescefi/sdk';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';

async function createMarket(
  connection: Connection,
  borrowerKeypair: Keypair,
  programId: PublicKey,
  usdcMint: PublicKey,
  params: {
    maxTotalSupply: bigint; // Max deposits (e.g., 100_000_000_000n for 100k USDC)
    annualInterestBps: number; // Interest rate in basis points (e.g., 800 = 8%)
    maturityTimestamp: bigint; // Unix timestamp when market matures
  }
) {
  const borrower = borrowerKeypair.publicKey;

  // 1. Get borrower's whitelist to find next market nonce
  const [whitelistPda] = findBorrowerWhitelistPda(borrower, programId);
  const whitelistAccount = await connection.getAccountInfo(whitelistPda);

  if (!whitelistAccount) {
    throw new Error('Borrower is not whitelisted');
  }

  decodeBorrowerWhitelist(whitelistAccount.data); // validates whitelist account layout
  const marketNonce = BigInt(Date.now()); // Use unique nonce for each market

  // 2. Derive all required PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const configAccount = await connection.getAccountInfo(configPda);
  if (!configAccount) throw new Error('Protocol config not found');
  const config = decodeProtocolConfig(configAccount.data);
  const blacklistProgram = configFieldToPublicKey(config.blacklistProgram);

  const [marketPda] = findMarketPda(borrower, marketNonce, programId);
  const [marketAuthorityPda] = findMarketAuthorityPda(marketPda, programId);
  const [vaultPda] = findVaultPda(marketPda, programId);
  const [blacklistCheckPda] = findBlacklistCheckPda(borrower, blacklistProgram);

  // 3. Build create market instruction
  const createMarketIx = createCreateMarketInstruction(
    {
      market: marketPda,
      borrower: borrower,
      mint: usdcMint,
      vault: vaultPda,
      marketAuthority: marketAuthorityPda,
      protocolConfig: configPda,
      borrowerWhitelist: whitelistPda,
      blacklistCheck: blacklistCheckPda,
      systemProgram: SystemProgram.programId,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    {
      marketNonce,
      annualInterestBps: params.annualInterestBps,
      maturityTimestamp: params.maturityTimestamp,
      maxTotalSupply: params.maxTotalSupply,
    },
    programId
  );

  // 4. Send transaction
  const tx = new Transaction().add(createMarketIx);
  const signature = await sendAndConfirmTransaction(connection, tx, [borrowerKeypair]);

  console.log('Market created:', marketPda.toBase58());
  console.log('Transaction:', signature);

  return { marketPda, vaultPda, signature };
}

// Usage
const maturityDate = new Date('2024-06-01T00:00:00Z');
await createMarket(connection, borrowerKeypair, programId, USDC_MINT, {
  maxTotalSupply: 100_000_000_000n, // 100,000 USDC (6 decimals)
  annualInterestBps: 800, // 8% APR
  maturityTimestamp: BigInt(Math.floor(maturityDate.getTime() / 1000)),
});

2. Deposit (Lender)

Lenders deposit funds into a market to earn interest.

import {
  findProtocolConfigPda,
  findMarketPda,
  findVaultPda,
  findLenderPositionPda,
  createDepositInstruction,
  decodeMarket,
} from '@coalescefi/sdk';
import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from '@solana/spl-token';

async function deposit(
  connection: Connection,
  lenderKeypair: Keypair,
  programId: PublicKey,
  marketPda: PublicKey,
  amount: bigint // Amount in token base units (e.g., 1_000_000n = 1 USDC)
) {
  const lender = lenderKeypair.publicKey;

  // 1. Fetch market to get mint
  const marketAccount = await connection.getAccountInfo(marketPda);
  if (!marketAccount) throw new Error('Market not found');
  const market = decodeMarket(marketAccount.data);

  // 2. Verify market is active (not matured)
  const now = BigInt(Math.floor(Date.now() / 1000));
  if (now >= market.maturityTimestamp) {
    throw new Error('Market has matured, deposits not allowed');
  }

  // 3. Derive required PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [vaultPda] = findVaultPda(marketPda, programId);
  const [positionPda] = findLenderPositionPda(marketPda, lender, programId);

  // 4. Get lender's token account
  const lenderTokenAccount = await getAssociatedTokenAddress(market.mint, lender);

  // 5. Derive blacklist check PDA (from external blacklist program)
  const blacklistCheckPda = PublicKey.default; // Replace with actual blacklist PDA derivation

  // 6. Build deposit instruction
  const depositIx = createDepositInstruction(
    {
      market: marketPda,
      lender: lender,
      lenderTokenAccount: lenderTokenAccount,
      vault: vaultPda,
      lenderPosition: positionPda,
      blacklistCheck: blacklistCheckPda,
      protocolConfig: configPda,
      mint: market.mint,
      tokenProgram: TOKEN_PROGRAM_ID,
      systemProgram: SystemProgram.programId,
    },
    {
      amount: amount,
    },
    programId
  );

  // 6. Send transaction
  const tx = new Transaction().add(depositIx);
  const signature = await sendAndConfirmTransaction(connection, tx, [lenderKeypair]);

  console.log('Deposited:', amount.toString(), 'to market:', marketPda.toBase58());
  console.log('Transaction:', signature);

  return { positionPda, signature };
}

// Usage: Deposit 1,000 USDC
await deposit(connection, lenderKeypair, programId, marketPda, 1_000_000_000n);

3. Borrow (Borrower)

Borrowers withdraw deposited funds from their markets.

import {
  findProtocolConfigPda,
  findMarketAuthorityPda,
  findVaultPda,
  findBorrowerWhitelistPda,
  findBlacklistCheckPda,
  createBorrowInstruction,
  decodeProtocolConfig,
  configFieldToPublicKey,
  decodeMarket,
} from '@coalescefi/sdk';

async function borrow(
  connection: Connection,
  borrowerKeypair: Keypair,
  programId: PublicKey,
  marketPda: PublicKey,
  amount: bigint
) {
  const borrower = borrowerKeypair.publicKey;

  // 1. Fetch market
  const marketAccount = await connection.getAccountInfo(marketPda);
  if (!marketAccount) throw new Error('Market not found');
  const market = decodeMarket(marketAccount.data);

  // 2. Verify borrower owns this market
  if (!market.borrower.equals(borrower)) {
    throw new Error('Only the market borrower can borrow');
  }

  // 3. Derive required PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [marketAuthorityPda] = findMarketAuthorityPda(marketPda, programId);
  const [vaultPda] = findVaultPda(marketPda, programId);
  const [whitelistPda] = findBorrowerWhitelistPda(borrower, programId);
  const configAccount = await connection.getAccountInfo(configPda);
  if (!configAccount) throw new Error('Protocol config not found');
  const config = decodeProtocolConfig(configAccount.data);
  const blacklistProgram = configFieldToPublicKey(config.blacklistProgram);
  const [blacklistCheckPda] = findBlacklistCheckPda(borrower, blacklistProgram);

  // 4. Get borrower's token account
  const borrowerTokenAccount = await getAssociatedTokenAddress(market.mint, borrower);

  // 5. Build borrow instruction
  const borrowIx = createBorrowInstruction(
    {
      market: marketPda,
      borrower: borrower,
      borrowerTokenAccount: borrowerTokenAccount,
      vault: vaultPda,
      marketAuthority: marketAuthorityPda,
      borrowerWhitelist: whitelistPda,
      blacklistCheck: blacklistCheckPda,
      protocolConfig: configPda,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    {
      amount: amount,
    },
    programId
  );

  // 6. Send transaction
  const tx = new Transaction().add(borrowIx);
  const signature = await sendAndConfirmTransaction(connection, tx, [borrowerKeypair]);

  console.log('Borrowed:', amount.toString(), 'from market:', marketPda.toBase58());
  console.log('Transaction:', signature);

  return { signature };
}

// Usage: Borrow 50,000 USDC
await borrow(connection, borrowerKeypair, programId, marketPda, 50_000_000_000n);

4. Repay Principal (Borrower)

Borrowers repay the principal to the market vault.

import {
  findProtocolConfigPda,
  findVaultPda,
  findBorrowerWhitelistPda,
  createRepayInstruction,
  decodeMarket,
} from '@coalescefi/sdk';

async function repay(
  connection: Connection,
  borrowerKeypair: Keypair,
  programId: PublicKey,
  marketPda: PublicKey,
  amount: bigint
) {
  const borrower = borrowerKeypair.publicKey;

  // 1. Fetch market
  const marketAccount = await connection.getAccountInfo(marketPda);
  if (!marketAccount) throw new Error('Market not found');
  const market = decodeMarket(marketAccount.data);

  // 2. Derive required PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [vaultPda] = findVaultPda(marketPda, programId);
  const [whitelistPda] = findBorrowerWhitelistPda(borrower, programId);

  // 3. Get borrower's token account
  const borrowerTokenAccount = await getAssociatedTokenAddress(market.mint, borrower);

  // 4. Build repay instruction (anyone can repay — payer need not be the borrower)
  const repayIx = createRepayInstruction(
    {
      market: marketPda,
      payer: borrower,
      payerTokenAccount: borrowerTokenAccount,
      vault: vaultPda,
      protocolConfig: configPda,
      mint: market.mint,
      borrowerWhitelist: whitelistPda,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    {
      amount: amount,
    },
    programId
  );

  // 5. Send transaction
  const tx = new Transaction().add(repayIx);
  const signature = await sendAndConfirmTransaction(connection, tx, [borrowerKeypair]);

  console.log('Repaid:', amount.toString(), 'to market:', marketPda.toBase58());
  console.log('Transaction:', signature);

  return { signature };
}

// Usage: Repay 50,000 USDC principal
await repay(connection, borrowerKeypair, programId, marketPda, 50_000_000_000n);

5. Repay Interest (Borrower)

Borrowers repay accrued interest separately from principal.

import {
  findProtocolConfigPda,
  findVaultPda,
  createRepayInterestInstruction,
  decodeMarket,
} from '@coalescefi/sdk';
import { calculateNormalizedAmount, estimateInterestAccrual } from '@coalescefi/shared-core';

async function repayInterest(
  connection: Connection,
  borrowerKeypair: Keypair,
  programId: PublicKey,
  marketPda: PublicKey,
  amount: bigint // Interest amount to repay
) {
  const borrower = borrowerKeypair.publicKey;

  // 1. Fetch market
  const marketAccount = await connection.getAccountInfo(marketPda);
  if (!marketAccount) throw new Error('Market not found');
  const market = decodeMarket(marketAccount.data);

  // 2. Derive required PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [vaultPda] = findVaultPda(marketPda, programId);

  // 3. Get borrower's token account
  const borrowerTokenAccount = await getAssociatedTokenAddress(market.mint, borrower);

  // 4. Build repay interest instruction
  const repayInterestIx = createRepayInterestInstruction(
    {
      market: marketPda,
      payer: borrower,
      payerTokenAccount: borrowerTokenAccount,
      vault: vaultPda,
      protocolConfig: configPda,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    {
      amount: amount,
    },
    programId
  );

  // 5. Send transaction
  const tx = new Transaction().add(repayInterestIx);
  const signature = await sendAndConfirmTransaction(connection, tx, [borrowerKeypair]);

  console.log('Repaid interest:', amount.toString());
  console.log('Transaction:', signature);

  return { signature };
}

// Helper: Calculate interest owed
async function calculateInterestOwed(
  connection: Connection,
  marketPda: PublicKey
): Promise<bigint> {
  const marketAccount = await connection.getAccountInfo(marketPda);
  if (!marketAccount) throw new Error('Market not found');
  const market = decodeMarket(marketAccount.data);

  const now = BigInt(Math.floor(Date.now() / 1000));
  const { newScaleFactor } = estimateInterestAccrual(market, now);
  const currentDebt = calculateNormalizedAmount(market.scaledTotalSupply, market.scaleFactor);
  const projectedDebt = calculateNormalizedAmount(market.scaledTotalSupply, newScaleFactor);
  const interestOwed = projectedDebt > currentDebt ? projectedDebt - currentDebt : 0n;

  return interestOwed;
}

// Usage
const interestOwed = await calculateInterestOwed(connection, marketPda);
await repayInterest(connection, borrowerKeypair, programId, marketPda, interestOwed);

6. Withdraw (Lender)

Lenders withdraw funds after market maturity. The first post-maturity withdrawal triggers settlement and locks the settlement factor.

import {
  findProtocolConfigPda,
  findMarketAuthorityPda,
  findVaultPda,
  findLenderPositionPda,
  findBlacklistCheckPda,
  createWithdrawInstruction,
  decodeProtocolConfig,
  configFieldToPublicKey,
  decodeMarket,
  decodeLenderPosition,
} from '@coalescefi/sdk';
import { calculateNormalizedAmount } from '@coalescefi/shared-core';

async function withdraw(
  connection: Connection,
  lenderKeypair: Keypair,
  programId: PublicKey,
  marketPda: PublicKey,
  minPayout?: bigint // Optional slippage protection
) {
  const lender = lenderKeypair.publicKey;

  // 1. Fetch market and position
  const [marketAccount, positionAccount] = await connection.getMultipleAccountsInfo([
    marketPda,
    findLenderPositionPda(marketPda, lender, programId)[0],
  ]);

  if (!marketAccount) throw new Error('Market not found');
  if (!positionAccount) throw new Error('Position not found');

  const market = decodeMarket(marketAccount.data);
  const position = decodeLenderPosition(positionAccount.data);

  // 2. Verify market is mature (first withdraw can trigger settlement)
  const now = BigInt(Math.floor(Date.now() / 1000));
  if (now < market.maturityTimestamp) {
    throw new Error('Market not yet matured');
  }

  // 3. Calculate expected payout if settlement is already locked
  const normalizedValue = calculateNormalizedAmount(position.scaledBalance, market.scaleFactor);
  const expectedPayout =
    market.settlementFactorWad > 0n ? (normalizedValue * market.settlementFactorWad) / WAD : null;

  console.log('Position shares:', position.scaledBalance.toString());
  if (expectedPayout !== null) {
    console.log('Expected payout:', expectedPayout.toString());
  } else {
    console.log('Settlement factor not locked yet; first withdraw call will set it.');
  }

  // 4. Derive required PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [marketAuthorityPda] = findMarketAuthorityPda(marketPda, programId);
  const [vaultPda] = findVaultPda(marketPda, programId);
  const [positionPda] = findLenderPositionPda(marketPda, lender, programId);
  const configAccount = await connection.getAccountInfo(configPda);
  if (!configAccount) throw new Error('Protocol config not found');
  const config = decodeProtocolConfig(configAccount.data);
  const blacklistProgram = configFieldToPublicKey(config.blacklistProgram);
  const [blacklistCheckPda] = findBlacklistCheckPda(lender, blacklistProgram);

  // 5. Get lender's token account
  const lenderTokenAccount = await getAssociatedTokenAddress(market.mint, lender);

  // 6. Build withdraw instruction
  const withdrawIx = createWithdrawInstruction(
    {
      market: marketPda,
      lender: lender,
      lenderTokenAccount: lenderTokenAccount,
      vault: vaultPda,
      lenderPosition: positionPda,
      marketAuthority: marketAuthorityPda,
      blacklistCheck: blacklistCheckPda,
      protocolConfig: configPda,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    {
      scaledAmount: 0n, // 0n = withdraw full position
      minPayout: minPayout, // Optional: revert if payout below this
    },
    programId
  );

  // 7. Send transaction
  const tx = new Transaction().add(withdrawIx);
  const signature = await sendAndConfirmTransaction(connection, tx, [lenderKeypair]);

  console.log('Withdrawn from market:', marketPda.toBase58());
  console.log('Transaction:', signature);

  return { expectedPayout, signature };
}

// Usage: Withdraw with 1% slippage tolerance
const expectedPayout = 1_050_000_000n; // Expected ~1,050 USDC
const minPayout = (expectedPayout * 99n) / 100n; // Allow 1% slippage
await withdraw(connection, lenderKeypair, programId, marketPda, minPayout);

7. Re-Settle (Anyone)

Update settlement factor if borrower makes late repayments.

import { findVaultPda, createReSettleInstruction, decodeMarket } from '@coalescefi/sdk';

async function reSettle(
  connection: Connection,
  payerKeypair: Keypair, // Anyone can call this
  programId: PublicKey,
  marketPda: PublicKey
) {
  // 1. Fetch market to verify it's settled
  const marketAccount = await connection.getAccountInfo(marketPda);
  if (!marketAccount) throw new Error('Market not found');
  const market = decodeMarket(marketAccount.data);

  if (market.settlementFactorWad === 0n) {
    throw new Error('Market not yet settled');
  }

  // 2. Derive required PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [vaultPda] = findVaultPda(marketPda, programId);

  // 3. Build re-settle instruction
  const reSettleIx = createReSettleInstruction(
    {
      market: marketPda,
      vault: vaultPda,
      protocolConfig: configPda,
    },
    programId
  );

  // 4. Send transaction
  const tx = new Transaction().add(reSettleIx);
  const signature = await sendAndConfirmTransaction(connection, tx, [payerKeypair]);

  console.log('Re-settled market:', marketPda.toBase58());
  console.log('Transaction:', signature);

  return { signature };
}

8. Collect Fees (Fee Authority)

Fee authority collects accrued protocol fees.

import {
  findProtocolConfigPda,
  findMarketAuthorityPda,
  findVaultPda,
  createCollectFeesInstruction,
} from '@coalescefi/sdk';

async function collectFees(
  connection: Connection,
  feeAuthorityKeypair: Keypair,
  programId: PublicKey,
  marketPda: PublicKey,
  feeRecipient: PublicKey
) {
  const feeAuthority = feeAuthorityKeypair.publicKey;

  // 1. Fetch market
  const marketAccount = await connection.getAccountInfo(marketPda);
  if (!marketAccount) throw new Error('Market not found');
  const market = decodeMarket(marketAccount.data);

  // 2. Derive required PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [marketAuthorityPda] = findMarketAuthorityPda(marketPda, programId);
  const [vaultPda] = findVaultPda(marketPda, programId);

  // 3. Get fee recipient's token account
  const feeTokenAccount = await getAssociatedTokenAddress(market.mint, feeRecipient);

  // 4. Build collect fees instruction
  const collectFeesIx = createCollectFeesInstruction(
    {
      market: marketPda,
      protocolConfig: configPda,
      feeAuthority: feeAuthorityKeypair.publicKey,
      feeTokenAccount: feeTokenAccount,
      vault: vaultPda,
      marketAuthority: marketAuthorityPda,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    programId
  );

  // 5. Send transaction
  const tx = new Transaction().add(collectFeesIx);
  const signature = await sendAndConfirmTransaction(connection, tx, [feeAuthorityKeypair]);

  console.log('Collected fees from market:', marketPda.toBase58());
  console.log('Transaction:', signature);

  return { signature };
}

9. Close Lender Position (Lender)

Close an empty position account to reclaim rent.

import {
  findProtocolConfigPda,
  findLenderPositionPda,
  createCloseLenderPositionInstruction,
  decodeLenderPosition,
} from '@coalescefi/sdk';
import { SystemProgram } from '@solana/web3.js';

async function closeLenderPosition(
  connection: Connection,
  lenderKeypair: Keypair,
  programId: PublicKey,
  marketPda: PublicKey
) {
  const lender = lenderKeypair.publicKey;

  // 1. Derive position PDA
  const [positionPda] = findLenderPositionPda(marketPda, lender, programId);
  const [configPda] = findProtocolConfigPda(programId);

  // 2. Verify position is empty
  const positionAccount = await connection.getAccountInfo(positionPda);
  if (!positionAccount) throw new Error('Position not found');
  const position = decodeLenderPosition(positionAccount.data);

  if (position.scaledBalance > 0n) {
    throw new Error('Position still has balance, withdraw first');
  }

  // 3. Build close position instruction
  const closeIx = createCloseLenderPositionInstruction(
    {
      market: marketPda,
      lenderPosition: positionPda,
      lender: lender,
      systemProgram: SystemProgram.programId,
      protocolConfig: configPda,
    },
    programId
  );

  // 4. Send transaction
  const tx = new Transaction().add(closeIx);
  const signature = await sendAndConfirmTransaction(connection, tx, [lenderKeypair]);

  console.log('Closed position, rent returned');
  console.log('Transaction:', signature);

  return { signature };
}

Reading Data

Reading Individual Accounts

The SDK provides decode functions for each account type: decodeProtocolConfig, decodeMarket, decodeLenderPosition, and decodeBorrowerWhitelist. Derive the PDA, fetch via connection.getAccountInfo(), and decode. See Account Decoders for examples.

Batch Reading

Multiple Accounts at Once

async function getMultipleMarkets(connection: Connection, marketPdas: PublicKey[]) {
  const accountInfos = await connection.getMultipleAccountsInfo(marketPdas);

  return accountInfos
    .map((info, index) => {
      if (!info) return null;
      return {
        address: marketPdas[index],
        data: decodeMarket(info.data),
      };
    })
    .filter(Boolean);
}

Market with Position

async function getMarketWithPosition(
  connection: Connection,
  marketPda: PublicKey,
  lender: PublicKey,
  programId: PublicKey
) {
  const [positionPda] = findLenderPositionPda(marketPda, lender, programId);

  const [marketInfo, positionInfo] = await connection.getMultipleAccountsInfo([
    marketPda,
    positionPda,
  ]);

  const market = marketInfo ? decodeMarket(marketInfo.data) : null;
  const position = positionInfo ? decodeLenderPosition(positionInfo.data) : null;

  return { market, position };
}

Calculating Current Values

Lender's Current Value

import { calculateNormalizedAmount } from '@coalescefi/shared-core';

async function getLenderValue(
  connection: Connection,
  marketPda: PublicKey,
  lender: PublicKey,
  programId: PublicKey
) {
  const { market, position } = await getMarketWithPosition(
    connection,
    marketPda,
    lender,
    programId
  );

  if (!market || !position) {
    return { shares: 0n, value: 0n };
  }

  const currentValue = calculateNormalizedAmount(position.scaledBalance, market.scaleFactor);

  return {
    shares: position.scaledBalance,
    scaleFactor: market.scaleFactor,
    currentValue,
  };
}

Market Statistics

import {
  calculateAvailableVaultBalance,
  calculateNormalizedAmount,
  calculateUtilizationRate,
} from '@coalescefi/shared-core';

async function getMarketStats(connection: Connection, marketPda: PublicKey) {
  const accountInfo = await connection.getAccountInfo(marketPda);
  if (!accountInfo) return null;

  const market = decodeMarket(accountInfo.data);
  const now = BigInt(Math.floor(Date.now() / 1000));

  const isMatured = now >= market.maturityTimestamp;
  const outstandingBorrowed =
    market.totalBorrowed > market.totalRepaid ? market.totalBorrowed - market.totalRepaid : 0n;
  const totalSupply = calculateNormalizedAmount(market.scaledTotalSupply, market.scaleFactor);
  const availableVaultBalance = calculateAvailableVaultBalance(market);
  const utilization = calculateUtilizationRate(market);
  const utilizationRate = totalSupply > 0n ? Number(utilization.bps) / 100 : 0;
  const fillRate =
    market.maxTotalSupply > 0n ? Number((totalSupply * 10000n) / market.maxTotalSupply) / 100 : 0;

  return {
    capacity: market.maxTotalSupply,
    totalSupply,
    outstandingBorrowed,
    available: availableVaultBalance,
    cumulativeDeposited: market.totalDeposited,
    cumulativeBorrowed: market.totalBorrowed,
    cumulativeRepaid: market.totalRepaid,
    apr: market.annualInterestBps,
    maturity: new Date(Number(market.maturityTimestamp) * 1000),
    isMatured,
    utilizationRate,
    fillRate,
    scaleFactor: market.scaleFactor,
    settlementFactor: market.settlementFactorWad,
  };
}

Subscribing to Account Changes

function subscribeToMarket(
  connection: Connection,
  marketPda: PublicKey,
  callback: (market: Market) => void
) {
  const subscriptionId = connection.onAccountChange(
    marketPda,
    (accountInfo) => {
      const market = decodeMarket(accountInfo.data);
      callback(market);
    },
    'confirmed'
  );

  // Return unsubscribe function
  return () => {
    connection.removeAccountChangeListener(subscriptionId);
  };
}

// Usage
const unsubscribe = subscribeToMarket(connection, marketPda, (market) => {
  console.log('Market updated:', market.scaleFactor);
});

// Later: cleanup
unsubscribe();

Finding All User Positions

async function findAllPositions(
  connection: Connection,
  lender: PublicKey,
  knownMarkets: PublicKey[],
  programId: PublicKey
) {
  const positionPdas = knownMarkets.map(
    (market) => findLenderPositionPda(market, lender, programId)[0]
  );

  const accountInfos = await connection.getMultipleAccountsInfo(positionPdas);

  const positions = [];
  for (let i = 0; i < accountInfos.length; i++) {
    if (accountInfos[i]) {
      positions.push({
        market: knownMarkets[i],
        position: decodeLenderPosition(accountInfos[i]!.data),
      });
    }
  }

  return positions;
}

Account Decoders

Parse Market

import { decodeMarket } from '@coalescefi/sdk';

const marketAccount = await connection.getAccountInfo(marketPda);
const market = decodeMarket(marketAccount!.data);

console.log({
  borrower: market.borrower.toBase58(),
  mint: market.mint.toBase58(),
  maxTotalSupply: market.maxTotalSupply,
  totalDeposited: market.totalDeposited,
  totalBorrowed: market.totalBorrowed,
  annualInterestBps: market.annualInterestBps,
  maturityTimestamp: market.maturityTimestamp,
  scaleFactor: market.scaleFactor,
  settlementFactorWad: market.settlementFactorWad,
  marketNonce: market.marketNonce,
});

Parse Lender Position

import { decodeLenderPosition } from '@coalescefi/sdk';

const positionAccount = await connection.getAccountInfo(positionPda);
const position = decodeLenderPosition(positionAccount!.data);

console.log({
  market: position.market.toBase58(),
  lender: position.lender.toBase58(),
  scaledBalance: position.scaledBalance,
  bump: position.bump,
});

Parse Borrower Whitelist

import { decodeBorrowerWhitelist } from '@coalescefi/sdk';

const whitelistAccount = await connection.getAccountInfo(whitelistPda);
const whitelist = decodeBorrowerWhitelist(whitelistAccount!.data);

console.log({
  borrower: whitelist.borrower.toBase58(),
  isWhitelisted: whitelist.isWhitelisted,
  maxBorrowCapacity: whitelist.maxBorrowCapacity,
  currentBorrowed: whitelist.currentBorrowed,
});

Parse Protocol Config

import { decodeProtocolConfig } from '@coalescefi/sdk';
import { PublicKey } from '@solana/web3.js';

const configAccount = await connection.getAccountInfo(configPda);
const config = decodeProtocolConfig(configAccount!.data);

console.log({
  admin: new PublicKey(config.admin).toBase58(),
  isPaused: config.isPaused,
  feeRateBps: config.feeRateBps,
  feeAuthority: new PublicKey(config.feeAuthority).toBase58(),
});

Squads Multisig Integration

For institutional borrowers and DAOs, Coalesce Finance operations can be executed through Squads Protocol multisig wallets.

Setup

import * as multisig from '@sqds/multisig';
import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from '@solana/web3.js';

// Your Squads multisig address
const MULTISIG_PDA = new PublicKey('YOUR_MULTISIG_PDA');

// Derive the vault PDA (this is the "signer" for your transactions)
const [vaultPda] = multisig.getVaultPda({
  multisigPda: MULTISIG_PDA,
  index: 0, // Usually vault index 0
});

console.log('Multisig Vault (use as borrower):', vaultPda.toBase58());

Create Market via Multisig

import * as multisig from '@sqds/multisig';
import {
  findProtocolConfigPda,
  findMarketPda,
  findMarketAuthorityPda,
  findVaultPda as findCoalesceVaultPda,
  findBorrowerWhitelistPda,
  createCreateMarketInstruction,
  decodeBorrowerWhitelist,
} from '@coalescefi/sdk';

async function createMarketViaMultisig(
  connection: Connection,
  memberKeypair: Keypair, // Multisig member proposing
  multisigPda: PublicKey,
  programId: PublicKey,
  usdcMint: PublicKey,
  params: {
    maxTotalSupply: bigint;
    annualInterestBps: number;
    maturityTimestamp: bigint;
  }
) {
  // 1. Get multisig vault (this is the "borrower")
  const [vaultPda] = multisig.getVaultPda({
    multisigPda: multisigPda,
    index: 0,
  });

  // 2. Verify borrower whitelist exists
  const [whitelistPda] = findBorrowerWhitelistPda(vaultPda, programId);
  const whitelistAccount = await connection.getAccountInfo(whitelistPda);

  if (!whitelistAccount) {
    throw new Error('Multisig vault is not whitelisted as borrower');
  }

  const whitelist = decodeBorrowerWhitelist(whitelistAccount.data);
  const marketNonce = BigInt(Date.now()); // Use unique nonce for each market

  // 3. Derive Coalesce Finance PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [marketPda] = findMarketPda(vaultPda, marketNonce, programId);
  const [marketAuthorityPda] = findMarketAuthorityPda(marketPda, programId);
  const [coalesceVaultPda] = findCoalesceVaultPda(marketPda, programId);

  // 4. Build the Coalesce Finance create market instruction
  const createMarketIx = createCreateMarketInstruction(
    {
      market: marketPda,
      borrower: vaultPda, // Multisig vault is the borrower!
      mint: usdcMint,
      vault: coalesceVaultPda,
      marketAuthority: marketAuthorityPda,
      protocolConfig: configPda,
      borrowerWhitelist: whitelistPda,
      blacklistCheck: blacklistCheckPda,
      systemProgram: SystemProgram.programId,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    {
      marketNonce,
      annualInterestBps: params.annualInterestBps,
      maturityTimestamp: params.maturityTimestamp,
      maxTotalSupply: params.maxTotalSupply,
    },
    programId
  );

  // 5. Get current multisig state for transaction index
  const multisigAccount = await multisig.accounts.Multisig.fromAccountAddress(
    connection,
    multisigPda
  );
  const transactionIndex = Number(multisigAccount.transactionIndex) + 1;

  // 6. Create vault transaction message
  const { blockhash } = await connection.getLatestBlockhash();

  const vaultTransactionMessage = new TransactionMessage({
    payerKey: vaultPda,
    recentBlockhash: blockhash,
    instructions: [createMarketIx],
  });

  // 7. Create the vault transaction (proposal)
  const createVaultTxIx = multisig.instructions.vaultTransactionCreate({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    creator: memberKeypair.publicKey,
    vaultIndex: 0,
    ephemeralSigners: 0,
    transactionMessage: vaultTransactionMessage,
  });

  // 8. Create proposal for the vault transaction
  const createProposalIx = multisig.instructions.proposalCreate({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    creator: memberKeypair.publicKey,
  });

  // 9. Auto-approve as creator
  const approveIx = multisig.instructions.proposalApprove({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    member: memberKeypair.publicKey,
  });

  // 10. Send proposal transaction
  const proposalTx = new VersionedTransaction(
    new TransactionMessage({
      payerKey: memberKeypair.publicKey,
      recentBlockhash: blockhash,
      instructions: [createVaultTxIx, createProposalIx, approveIx],
    }).compileToV0Message()
  );

  proposalTx.sign([memberKeypair]);
  const signature = await connection.sendTransaction(proposalTx);
  await connection.confirmTransaction(signature);

  console.log('Proposal created, transaction index:', transactionIndex);
  console.log('Signature:', signature);

  return { transactionIndex, marketPda, signature };
}

Approve Multisig Proposal

async function approveProposal(
  connection: Connection,
  memberKeypair: Keypair,
  multisigPda: PublicKey,
  transactionIndex: number
) {
  const approveIx = multisig.instructions.proposalApprove({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    member: memberKeypair.publicKey,
  });

  const { blockhash } = await connection.getLatestBlockhash();

  const tx = new VersionedTransaction(
    new TransactionMessage({
      payerKey: memberKeypair.publicKey,
      recentBlockhash: blockhash,
      instructions: [approveIx],
    }).compileToV0Message()
  );

  tx.sign([memberKeypair]);
  const signature = await connection.sendTransaction(tx);
  await connection.confirmTransaction(signature);

  console.log('Approved proposal:', transactionIndex);
  return { signature };
}

Execute Approved Proposal

async function executeProposal(
  connection: Connection,
  payerKeypair: Keypair, // Anyone can execute once approved
  multisigPda: PublicKey,
  transactionIndex: number
) {
  // 1. Get the vault transaction PDA
  const [transactionPda] = multisig.getTransactionPda({
    multisigPda: multisigPda,
    index: BigInt(transactionIndex),
  });

  // 2. Get proposal PDA
  const [proposalPda] = multisig.getProposalPda({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
  });

  // 3. Check proposal is approved
  const proposal = await multisig.accounts.Proposal.fromAccountAddress(connection, proposalPda);

  if (proposal.status.__kind !== 'Approved') {
    throw new Error(`Proposal not approved. Status: ${proposal.status.__kind}`);
  }

  // 4. Execute the vault transaction
  const executeIx = multisig.instructions.vaultTransactionExecute({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    member: payerKeypair.publicKey,
  });

  const { blockhash } = await connection.getLatestBlockhash();

  const tx = new VersionedTransaction(
    new TransactionMessage({
      payerKey: payerKeypair.publicKey,
      recentBlockhash: blockhash,
      instructions: [executeIx],
    }).compileToV0Message()
  );

  tx.sign([payerKeypair]);
  const signature = await connection.sendTransaction(tx);
  await connection.confirmTransaction(signature);

  console.log('Executed proposal:', transactionIndex);
  console.log('Signature:', signature);

  return { signature };
}

Borrow via Multisig

async function borrowViaMultisig(
  connection: Connection,
  memberKeypair: Keypair,
  multisigPda: PublicKey,
  programId: PublicKey,
  marketPda: PublicKey,
  amount: bigint
) {
  // 1. Get multisig vault
  const [vaultPda] = multisig.getVaultPda({
    multisigPda: multisigPda,
    index: 0,
  });

  // 2. Fetch market
  const marketAccount = await connection.getAccountInfo(marketPda);
  if (!marketAccount) throw new Error('Market not found');
  const market = decodeMarket(marketAccount.data);

  // 3. Derive PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [marketAuthorityPda] = findMarketAuthorityPda(marketPda, programId);
  const [coalesceVaultPda] = findCoalesceVaultPda(marketPda, programId);
  const [whitelistPda] = findBorrowerWhitelistPda(vaultPda, programId);

  // 4. Get vault's token account
  const vaultTokenAccount = await getAssociatedTokenAddress(market.mint, vaultPda, true);

  // 5. Build borrow instruction
  const borrowIx = createBorrowInstruction(
    {
      market: marketPda,
      borrower: vaultPda,
      borrowerTokenAccount: vaultTokenAccount,
      vault: coalesceVaultPda,
      marketAuthority: marketAuthorityPda,
      borrowerWhitelist: whitelistPda,
      blacklistCheck: blacklistCheckPda,
      protocolConfig: configPda,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    { amount },
    programId
  );

  // 6. Create proposal (same pattern as createMarketViaMultisig)
  const multisigAccount = await multisig.accounts.Multisig.fromAccountAddress(
    connection,
    multisigPda
  );
  const transactionIndex = Number(multisigAccount.transactionIndex) + 1;

  const { blockhash } = await connection.getLatestBlockhash();

  const vaultTransactionMessage = new TransactionMessage({
    payerKey: vaultPda,
    recentBlockhash: blockhash,
    instructions: [borrowIx],
  });

  const createVaultTxIx = multisig.instructions.vaultTransactionCreate({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    creator: memberKeypair.publicKey,
    vaultIndex: 0,
    ephemeralSigners: 0,
    transactionMessage: vaultTransactionMessage,
  });

  const createProposalIx = multisig.instructions.proposalCreate({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    creator: memberKeypair.publicKey,
  });

  const approveIx = multisig.instructions.proposalApprove({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    member: memberKeypair.publicKey,
  });

  const proposalTx = new VersionedTransaction(
    new TransactionMessage({
      payerKey: memberKeypair.publicKey,
      recentBlockhash: blockhash,
      instructions: [createVaultTxIx, createProposalIx, approveIx],
    }).compileToV0Message()
  );

  proposalTx.sign([memberKeypair]);
  const signature = await connection.sendTransaction(proposalTx);
  await connection.confirmTransaction(signature);

  console.log('Borrow proposal created, index:', transactionIndex);
  return { transactionIndex, signature };
}

Repay via Multisig

async function repayViaMultisig(
  connection: Connection,
  memberKeypair: Keypair,
  multisigPda: PublicKey,
  programId: PublicKey,
  marketPda: PublicKey,
  principalAmount: bigint,
  interestAmount: bigint
) {
  // 1. Get multisig vault
  const [vaultPda] = multisig.getVaultPda({
    multisigPda: multisigPda,
    index: 0,
  });

  // 2. Fetch market
  const marketAccount = await connection.getAccountInfo(marketPda);
  if (!marketAccount) throw new Error('Market not found');
  const market = decodeMarket(marketAccount.data);

  // 3. Derive PDAs
  const [configPda] = findProtocolConfigPda(programId);
  const [coalesceVaultPda] = findCoalesceVaultPda(marketPda, programId);
  const [whitelistPda] = findBorrowerWhitelistPda(vaultPda, programId);

  // 4. Get vault's token account
  const vaultTokenAccount = await getAssociatedTokenAddress(market.mint, vaultPda, true);

  // 5. Build repay instructions (principal and interest are separate on-chain)
  //    Repay rejects amounts above current_borrowed (principal only),
  //    so interest must use createRepayInterestInstruction.
  const repayPrincipalIx = createRepayInstruction(
    {
      market: marketPda,
      payer: vaultPda,
      payerTokenAccount: vaultTokenAccount,
      vault: coalesceVaultPda,
      protocolConfig: configPda,
      mint: market.mint,
      borrowerWhitelist: whitelistPda,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    { amount: principalAmount },
    programId
  );

  const repayInterestIx = createRepayInterestInstruction(
    {
      market: marketPda,
      payer: vaultPda,
      payerTokenAccount: vaultTokenAccount,
      vault: coalesceVaultPda,
      protocolConfig: configPda,
      tokenProgram: TOKEN_PROGRAM_ID,
    },
    { amount: interestAmount },
    programId
  );

  // 6. Create proposal (same pattern)
  const multisigAccount = await multisig.accounts.Multisig.fromAccountAddress(
    connection,
    multisigPda
  );
  const transactionIndex = Number(multisigAccount.transactionIndex) + 1;

  const { blockhash } = await connection.getLatestBlockhash();

  const vaultTransactionMessage = new TransactionMessage({
    payerKey: vaultPda,
    recentBlockhash: blockhash,
    instructions: [repayPrincipalIx, repayInterestIx],
  });

  const createVaultTxIx = multisig.instructions.vaultTransactionCreate({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    creator: memberKeypair.publicKey,
    vaultIndex: 0,
    ephemeralSigners: 0,
    transactionMessage: vaultTransactionMessage,
  });

  const createProposalIx = multisig.instructions.proposalCreate({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    creator: memberKeypair.publicKey,
  });

  const approveIx = multisig.instructions.proposalApprove({
    multisigPda: multisigPda,
    transactionIndex: BigInt(transactionIndex),
    member: memberKeypair.publicKey,
  });

  const proposalTx = new VersionedTransaction(
    new TransactionMessage({
      payerKey: memberKeypair.publicKey,
      recentBlockhash: blockhash,
      instructions: [createVaultTxIx, createProposalIx, approveIx],
    }).compileToV0Message()
  );

  proposalTx.sign([memberKeypair]);
  const signature = await connection.sendTransaction(proposalTx);
  await connection.confirmTransaction(signature);

  console.log('Repay proposal created, index:', transactionIndex);
  return { transactionIndex, signature };
}

Complete Multisig Workflow Example

// Full workflow for a DAO borrowing via Squads

async function daoLoanWorkflow() {
  const connection = new Connection('https://api.mainnet-beta.solana.com');
  const programId = new PublicKey('COALESCEFI_PROGRAM_ID');
  const usdcMint = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');

  // DAO's Squads multisig
  const multisigPda = new PublicKey('YOUR_MULTISIG');

  // Member keypairs (in practice, each member signs independently)
  const member1 = Keypair.fromSecretKey(/* ... */);
  const member2 = Keypair.fromSecretKey(/* ... */);
  const member3 = Keypair.fromSecretKey(/* ... */);

  // Step 1: Create market (member1 proposes)
  console.log('\n=== Creating Market ===');
  const { transactionIndex: createTxIndex, marketPda } = await createMarketViaMultisig(
    connection,
    member1,
    multisigPda,
    programId,
    usdcMint,
    {
      maxTotalSupply: 500_000_000_000n, // 500k USDC
      annualInterestBps: 850, // 8.5% APR
      maturityTimestamp: BigInt(Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60), // 90 days
    }
  );

  // Step 2: Other members approve
  console.log('\n=== Collecting Approvals ===');
  await approveProposal(connection, member2, multisigPda, createTxIndex);
  await approveProposal(connection, member3, multisigPda, createTxIndex);

  // Step 3: Execute create market
  console.log('\n=== Executing Create Market ===');
  await executeProposal(connection, member1, multisigPda, createTxIndex);
  console.log('Market created:', marketPda.toBase58());

  // ... Wait for lender deposits ...

  // Step 4: Borrow funds (after deposits arrive)
  console.log('\n=== Creating Borrow Proposal ===');
  const { transactionIndex: borrowTxIndex } = await borrowViaMultisig(
    connection,
    member1,
    multisigPda,
    programId,
    marketPda,
    250_000_000_000n // Borrow 250k USDC
  );

  // Step 5: Approve borrow
  console.log('\n=== Approving Borrow ===');
  await approveProposal(connection, member2, multisigPda, borrowTxIndex);
  await approveProposal(connection, member3, multisigPda, borrowTxIndex);

  // Step 6: Execute borrow
  console.log('\n=== Executing Borrow ===');
  await executeProposal(connection, member1, multisigPda, borrowTxIndex);

  // ... Use funds, time passes ...

  // Step 7: Repay before maturity
  // On-chain Repay rejects amounts above current_borrowed (principal only),
  // so principal and interest must be repaid via separate instructions.
  console.log('\n=== Creating Repay Proposal ===');
  const principal = 250_000_000_000n;
  const interest = 5_312_500_000n;
  const { transactionIndex: repayTxIndex } = await repayViaMultisig(
    connection,
    member1,
    multisigPda,
    programId,
    marketPda,
    principal,
    interest
  );

  // Step 8: Approve and execute repay
  console.log('\n=== Approving & Executing Repay ===');
  await approveProposal(connection, member2, multisigPda, repayTxIndex);
  await approveProposal(connection, member3, multisigPda, repayTxIndex);
  await executeProposal(connection, member1, multisigPda, repayTxIndex);

  console.log('\n=== Loan Complete ===');
}

Math Utilities

For math calculations, use the @coalescefi/shared-core package. See the Shared Core reference for the full API including:

  • calculateScaledAmount / calculateNormalizedAmount — share-to-token conversion
  • calculateSettlementPayout — settled withdrawal payout from scaled shares
  • estimateInterestAccrual — preview market accrual values
  • Validation: validateDepositAmount, validateBorrowAmount, validateRepayAmount, validateWithdrawAmount
  • Formatting: formatTokenAmount, formatUSD, formatPercent

Quick example:

import {
  calculateScaledAmount,
  calculateNormalizedAmount,
  calculateSettlementPayout,
  estimateInterestAccrual,
  formatTokenAmount,
} from '@coalescefi/shared-core';

// Convert deposit amount to scaled shares
const scaledShares = calculateScaledAmount(depositAmount, scaleFactor);

// Convert shares back to token amount
const tokenAmount = calculateNormalizedAmount(scaledShares, scaleFactor);

// Apply settlement factor to normalized amount
const payout = calculateSettlementPayout(tokenAmount, settlementFactor);

// Preview interest accrual at current timestamp
const { newScaleFactor, interestDelta } = estimateInterestAccrual(market, currentTimestamp);

// Format for display
const displayAmount = formatTokenAmount(amount, 6); // "1,234.56"

Error Handling

Error Types

Program Errors

Errors from the on-chain program (error codes 0-42). See Error Codes Reference for the complete list.

RPC Errors

Network and connection errors: connection timeouts, node rate limits, invalid responses.

Client Errors

SDK and input validation errors: invalid public keys, wrong account types, serialization failures.

Catching Transaction Errors

Basic Pattern

import { SendTransactionError } from '@solana/web3.js';

try {
  const sig = await sendAndConfirmTransaction(connection, tx, [signer]);
  console.log('Success:', sig);
} catch (error) {
  if (error instanceof SendTransactionError) {
    console.error('Transaction failed');
    console.error('Logs:', error.logs);
  } else {
    console.error('Other error:', error);
  }
}

Extracting Error Code

function extractErrorCode(logs: string[]): number | null {
  // Look for "Program log: Error Code: X"
  for (const log of logs) {
    const match = log.match(/Error Code: (\d+)/);
    if (match) {
      return parseInt(match[1], 10);
    }
  }
  return null;
}

// Usage
try {
  await sendAndConfirmTransaction(connection, tx, [signer]);
} catch (error) {
  if (error instanceof SendTransactionError && error.logs) {
    const code = extractErrorCode(error.logs);
    if (code !== null) {
      console.log('Error code:', code);
      handleErrorCode(code);
    }
  }
}

Recoverable vs Non-Recoverable

function isRecoverableError(code: number): boolean {
  const recoverableErrors = [
    25, // CapExceeded - can retry with smaller amount
    26, // BorrowAmountTooHigh - wait for deposits
    8, // ProtocolPaused - wait for unpause
    32, // SettlementGracePeriod - wait 5 minutes
  ];

  return recoverableErrors.includes(code);
}

Quick Error Handling

import { CoalescefiError } from '@coalescefi/sdk';

try {
  const tx = new Transaction().add(depositIx);
  await sendAndConfirmTransaction(connection, tx, [wallet]);
} catch (error) {
  if (error.message.includes('MarketMatured')) {
    console.error('Cannot deposit: market has matured');
  } else if (error.message.includes('CapExceeded')) {
    console.error('Cannot deposit: market capacity reached');
  } else if (error.message.includes('Paused')) {
    console.error('Cannot deposit: protocol is paused');
  } else {
    throw error;
  }
}

Requirements

  • Node.js 18+
  • TypeScript 5.0+ (recommended)
  • @solana/web3.js ^1.91.0
  • @sqds/multisig (for multisig operations)