Solana Transactions Are Not What I Expected (Coming From EVM)
Source: Dev.to
What I thought I knew
On EVM chains, a transaction is mostly a wrapper. You have:
- A
toaddress - Some
value(ETH) - Optional
data(the encoded function call) - A signature from your wallet
The RPC figures out what to do with it. You don’t think too hard about the structure—ethers.js handles most of it.
Solana transactions look structurally similar at first glance. You sign something, you broadcast it. But once you actually build one from scratch, the differences become hard to ignore.
The anatomy is more explicit
A Solana transaction is made of instructions, and each instruction spells out exactly which accounts it touches and whether each account is a signer, writable, or read‑only. You don’t just call a function and let the runtime figure out state; you declare upfront: “This instruction reads from account A and writes to account B.”
Here’s what building a simple transfer looked like when I did it:
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: recipient,
lamports: LAMPORTS_PER_SOL * 0.001,
})
);
transaction.recentBlockhash = (
await connection.getLatestBlockhash()
).blockhash;
transaction.feePayer = sender.publicKey;
Three things you set manually that ethers.js would have handled for you: the instruction, the blockhash, and the fee payer. On Solana, you’re closer to the metal.
The blockhash thing caught me off guard
Every Solana transaction includes a recentBlockhash. It’s not optional. It ties your transaction to a specific point in time, and it expires. If your transaction doesn’t land within roughly 60–90 seconds, it’s dead. You have to rebuild it with a fresh blockhash and try again.
On EVM chains you deal with nonces and gas‑price wars; transactions can sit in the mempool for minutes. Solana’s model is intentionally short‑lived: either your transaction lands fast, or it doesn’t land at all.
I actually triggered this on devnet by fetching a blockhash, waiting too long, and then trying to send. The transaction came back invalid. Once I understood why, it made sense—Solana is optimizing for speed and finality, not for letting transactions queue.
Breaking transactions taught me more than building them
The task I was working through asked me to intentionally break transactions to understand the failure modes. That turned out to be the most useful part.
Some things I broke on purpose:
- Sending without enough SOL for rent‑exempt balance
- Exceeding the compute budget (Solana allocates compute units per transaction, similar to gas but more explicit)
- Including too many instructions in one transaction (Solana has a hard 1,232‑byte size limit on transactions)
That last one doesn’t have a direct EVM equivalent. On Ethereum you can pack a lot into calldata. On Solana, 1,232 bytes is the ceiling, full stop. It shapes how you design programs: if you need to do a lot, you batch across multiple transactions or restructure your logic.
Failed transactions show up on Solana Explorer with a clear error log, which is actually better than what I’m used to on EVM, where a reverted transaction sometimes just says “execution reverted” with nothing useful attached.
The mental model that finally clicked
On EVM, I think of a transaction as sending a message to a contract.
On Solana, it’s closer to submitting a batch of state changes that either all succeed or all fail. The accounts are the state, the instructions are the operations, and the transaction is the atomic unit that wraps them.
That’s not a huge conceptual leap, but the way you build for it is different. You think about accounts before you think about logic. You declare intent upfront instead of letting the runtime discover it.
I’m still early in this—Day 20 of 100. But transactions are the part where Solana stopped feeling like “Ethereum but faster” and started feeling like its own thing.
Building through the 100 Days of Solana challenge. If you’re doing the same, feel free to connect.