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/sdkFor 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 Version | Solana/web3.js | Program Version |
|---|---|---|
| 0.1.x | ^1.91.0 | v1 |
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');
Recommended Settings
| Use Case | Commitment |
|---|---|
| Reading market data | confirmed |
| Checking balances | confirmed |
| After transaction | confirmed |
| Critical operations | finalized |
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 conversioncalculateSettlementPayout— settled withdrawal payout from scaled sharesestimateInterestAccrual— 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)