SOL, Lamports & Units
Solana stores all native currency as lamports - unsigned 64-bit integers. Understanding denominations prevents rounding bugs, failed transactions, and incorrect fee calculations.
Recipe
Quick-reference recipe card - copy-paste ready.
import { lamports } from "@solana/kit";
const ONE_SOL = 1_000_000_000n;
const halfSol = lamports(500_000_000n);
function solToLamports(sol: number): bigint {
return BigInt(Math.round(sol * 1e9));
}
function lamportsToSol(lps: bigint): string {
return (Number(lps) / 1e9).toFixed(9);
}When to reach for this:
- Converting user-facing SOL amounts to on-chain lamports before building instructions
- Displaying balances fetched from RPC without precision loss
- Calculating rent-exempt minimum deposits for new accounts
- Comparing priority fees (microlamports per CU) against base fees (lamports per signature)
- Validating SPL token amounts against mint
decimals(separate from native SOL)
Working Example
import { createSolanaRpc, address, lamports } from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
import {
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
pipe,
} from "@solana/kit";
const rpc = createSolanaRpc("https://api.devnet.solana.com");
const sender = address("SENDER_PUBKEY");
const recipient = address("RECIPIENT_PUBKEY");
// User enters "0.25" SOL in a UI form
const userInputSol = 0.25;
const amountLamports = BigInt(Math.round(userInputSol * 1_000_000_000));
const { value: balance } = await rpc.getBalance(sender).send();
if (balance < amountLamports) {
throw new Error(
`Insufficient: have ${Number(lamports(balance)) / 1e9} SOL, need ${userInputSol}`,
);
}
const { value: blockhash } = await rpc.getLatestBlockhash().send();
const transferIx = getTransferSolInstruction({
source: sender,
destination: recipient,
amount: amountLamports,
});
const message = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayer(sender, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
(m) => appendTransactionMessageInstruction(transferIx, m),
);
console.log("Transfer message built for", amountLamports, "lamports");What this demonstrates:
- User input is converted to
bigintlamports before any on-chain comparison getBalancereturns lamports - compare integers, not floats- Kit's
lamports()helper wraps raw values for type clarity - Transaction amounts are always whole lamport integers
Deep Dive
How It Works
- The genesis and all runtime accounting use lamports as the atomic unit
- 1 SOL = 10^9 lamports - the same relationship as wei to ETH, but with 9 decimals
- Account balances, transfers, rent, and fees are all u64 lamport values
- Floating-point types cannot represent all lamport values exactly at scale
Denominations at a Glance
| Unit | Lamports | Typical Use |
|---|---|---|
| 1 lamport | 1 | Smallest native unit |
| 1 SOL | 1,000,000,000 | User-facing display |
| Priority fee | microlamports/CU | Compute Budget Program |
| Base fee | 5,000 lamports/signature | Per-signature cost (current default) |
Rust Notes
use anchor_lang::prelude::*;
const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
pub fn transfer_amount(ctx: Context<Transfer>, sol: f64) -> Result<()> {
let lamports = (sol * LAMPORTS_PER_SOL as f64) as u64; // avoid in production
// Prefer: user supplies lamports directly, or use a fixed-point crate
require!(lamports > 0, ErrorCode::ZeroAmount);
Ok(())
}- Never cast
f64tou64in production financial code - use integer input anchor_langprovidesLamportstype alias in some contexts- Use
checked_add,checked_subfor balance arithmetic in programs
Gotchas
- JavaScript float rounding -
0.1 + 0.2 !== 0.3can produce wrong lamport amounts. Fix: parse user strings tobigintvia integer math or a decimal library. - Displaying u64 in JS - values above
Number.MAX_SAFE_INTEGERlose precision. Fix: format lamports asbigintor use a string-based formatter. - Mixing SOL and lamports in APIs - some CLI flags use SOL, RPC returns lamports. Fix: read each API's docs and normalize at your app boundary.
- SPL token decimals differ - USDC uses 6 decimals, SOL uses 9. Fix: always read
mint.decimalsbefore converting token amounts. - Rent calculation in SOL - developers sometimes round rent to 0. Fix: use
getMinimumBalanceForRentExemptionRPC and store the exact lamport value. - Priority fee units - microlamports per CU is not the same as lamports per signature. Fix: set both via Compute Budget instructions separately.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
bigint in TypeScript | All on-chain amount handling | You need legacy browser support without polyfills |
Decimal.js / bignumber.js | Parsing user decimal strings safely | Inside on-chain Rust programs |
| Integer lamports only (no SOL type) | Program instruction args | Building user-facing wallet UIs |
@solana/kit lamports() helper | Type-safe Kit pipelines | Raw RPC scripts where Kit is overkill |
FAQs
How many lamports are in one SOL?
1,000,000,000 (10^9). This is fixed and never changes.
Why does Solana use integers instead of decimals on-chain?
Deterministic execution across all validators requires exact arithmetic. Floating point varies by hardware; u64 integers do not.
What is the smallest transferable amount of SOL?
1 lamport. In practice, transaction fees (5,000 lamports per signature by default) make sub-lamport transfers meaningless.
How do I safely convert a user-entered SOL string to lamports?
function parseSolToLamports(input: string): bigint {
const [whole, frac = ""] = input.split(".");
const padded = (frac + "000000000").slice(0, 9);
return BigInt(whole) * 1_000_000_000n + BigInt(padded);
}Does getBalance return SOL or lamports?
Lamports. Divide by 1e9 only for display - never for on-chain instructions.
What are microlamports in priority fees?
1 microlamport = 0.000001 lamports. Priority fees are priced in microlamports per compute unit via the Compute Budget Program.
Can lamport balances overflow u64?
Theoretically at ~18.4 billion SOL total supply it is not a practical concern, but program logic should still use checked arithmetic.
How is rent-exemption calculated?
Based on account data size in bytes. RPC getMinimumBalanceForRentExemption(dataSize) returns the exact lamport deposit required.
Should I store amounts as SOL or lamports in my database?
Store lamports as integers (or strings for very large values). Convert to SOL only in the presentation layer.
What happens if I send a transfer with amount 0?
The System Program rejects zero-lamport transfers. Use a meaningful minimum or skip the instruction.
Are SPL token amounts in lamports?
No. SPL tokens use the mint's decimals field. A USDC amount of 1_000_000 means 1 USDC (6 decimals), not lamports.
How do I format lamports for display without precision loss?
Divide bigint lamports into whole and fractional parts with string math, or use a library. Avoid Number(lamports) for values you will re-submit on-chain.
Related
- Solana Basics - orientation to accounts and transactions
- Keypairs & Addresses - who holds the lamports
- Clusters & Networks - devnet airdrop amounts
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.