Transaction Lifecycle
Every trade on Lavarage follows the same flow: your client requests a transaction from the API, signs it locally, and submits it to Solana. This page explains the full lifecycle so you can handle every state correctly.
The Flow
Client Lavarage API Solana
| | |
|-- POST /open-by-token --> | |
| |-- query offers |
| |-- select best pool |
| |-- build transaction |
| <-- { transaction } ---- | |
| | |
|-- sign(transaction) ----> | |
| | |
|-- sendTransaction --------|------------------------> |
| | |
| <-- signature -----------| |
| | <-- position detected |
| |-- sync position state |
Step 1: Request the Transaction
Call one of the position endpoints (open-by-token, close, split, etc.). The server:
- Validates your parameters
- Selects the best pool (for
open-by-token) or validates the position exists - Gets a Jupiter swap quote for the token swap
- Builds a Solana
VersionedTransactionwith all necessary instructions - Returns the transaction as a base58-encoded string
The response includes:
{
"transaction": "AQAAAAAAAAAA...",
"positionAddress": "7xKXtg...",
"lastValidBlockHeight": 285432100,
"quote": { "outAmount": "9950000000", "priceImpactPct": 0.12 }
}
The transaction is unsigned. Only the user's wallet can sign it.
Step 2: Sign the Transaction
Deserialize and sign on the client side:
import { VersionedTransaction } from '@solana/web3.js';
import bs58 from 'bs58';
const tx = VersionedTransaction.deserialize(bs58.decode(response.transaction));
tx.sign([userKeypair]);
// or for wallet adapters:
// const signed = await wallet.signTransaction(tx);
The transaction contains a recent blockhash. You have a limited window to submit before it expires (typically ~60-90 seconds).
Step 3: Submit to Solana
You have two options for submission:
Option A: Direct Submit (Simple)
const signature = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false,
maxRetries: 3,
});
Option B: Jito Bundle with MEV Protection (Recommended for Large Trades)
For trades that include MEV protection (via astralaneTipLamports), submit through the Lavarage bundle endpoint:
const { signature } = await fetch(`${API_BASE}/api/v1/bundle/submit`, {
method: 'POST',
headers,
body: JSON.stringify({
transactions: [bs58.encode(tx.serialize())],
mevProtect: true,
}),
}).then(r => r.json());
The bundle endpoint routes through Jito for MEV protection. Use this for any trade where sandwich attack prevention matters.
Option C: Multi-Transaction Operations (Partial Sell, etc.)
Some operations return multiple transactions that must be submitted together as a bundle:
// Partial sell returns splitTransaction + closeTransaction
const { splitTransaction, closeTransaction } = response;
const tx1 = VersionedTransaction.deserialize(bs58.decode(splitTransaction));
const tx2 = VersionedTransaction.deserialize(bs58.decode(closeTransaction));
tx1.sign([userKeypair]);
tx2.sign([userKeypair]);
await fetch(`${API_BASE}/api/v1/bundle/submit`, {
method: 'POST',
headers,
body: JSON.stringify({
transactions: [
bs58.encode(tx1.serialize()),
bs58.encode(tx2.serialize()),
],
}),
}).then(r => r.json());
Step 4: Confirm the Transaction
After submission, confirm the transaction landed:
const confirmation = await connection.confirmTransaction({
signature,
blockhash: tx.message.recentBlockhash,
lastValidBlockHeight: response.lastValidBlockHeight,
}, 'confirmed');
if (confirmation.value.err) {
console.error('Transaction failed:', confirmation.value.err);
}
Always use lastValidBlockHeight from the API response for confirmation. This ensures you don't wait forever if the blockhash expires.
Step 5: Position Sync
After the transaction confirms on-chain, Lavarage's listener detects the position event and syncs state to the database. This typically takes 1-5 seconds after confirmation.
During this window:
- The position may appear as
ONCHAIN(detected but not fully synced) - Once synced, it transitions to
EXECUTED - The position is now visible via
GET /api/v1/positions
Position States
| State | Meaning |
|---|---|
ONCHAIN | Transaction confirmed, position detected by listener |
EXECUTED | Position fully synced with on-chain state — all fields populated |
CLOSED | Close transaction confirmed |
CLOSED_EXECUTED | Close fully synced — final P&L calculated |
LIQUIDATED | Position was liquidated by the protocol |
Error Handling
Transaction Simulation Failed
The on-chain state changed between when the API built the transaction and when it was submitted. Common causes:
- Another trader took the pool's remaining liquidity
- Token price moved beyond slippage tolerance
Fix: Get a fresh quote and rebuild the transaction.
Blockhash Expired
The transaction took too long to submit (>60-90 seconds between API call and submission).
Fix: Rebuild the transaction. Don't cache transactions.
Insufficient Funds
The wallet doesn't have enough SOL for rent/fees, or doesn't have enough of the collateral token.
Fix: Check balances before calling the API. Every Solana transaction needs ~0.01 SOL for fees.
Transaction Too Large
Complex swap routes can make the transaction exceed Solana's size limit.
Fix: Reduce slippage (fewer alternative routes) or try with a different collateral amount.
Timing Best Practices
| Step | Max Safe Delay |
|---|---|
| API call to sign | Seconds (quote is fresh) |
| Sign to submit | <60 seconds (blockhash expiry) |
| Submit to confirm | 5-30 seconds (network dependent) |
| Confirm to position sync | 1-5 seconds |
The critical window is between getting the transaction and submitting it. Don't show the user a confirmation dialog that takes 2 minutes. Get the transaction, sign it, submit it. Show the preview from the quote endpoint separately.
Retry Strategy
async function submitWithRetry(
buildTx: () => Promise<{ transaction: string; lastValidBlockHeight: number }>,
sign: (tx: VersionedTransaction) => Promise<VersionedTransaction>,
maxRetries = 2
) {
for (let i = 0; i <= maxRetries; i++) {
const response = await buildTx();
const tx = VersionedTransaction.deserialize(bs58.decode(response.transaction));
const signed = await sign(tx);
try {
const sig = await connection.sendRawTransaction(signed.serialize());
const confirmation = await connection.confirmTransaction({
signature: sig,
blockhash: signed.message.recentBlockhash,
lastValidBlockHeight: response.lastValidBlockHeight,
}, 'confirmed');
if (!confirmation.value.err) return sig;
} catch (e) {
if (i === maxRetries) throw e;
// Rebuild transaction on next iteration (fresh blockhash + quote)
}
}
}
The key insight: always rebuild the transaction on retry. Never resubmit the same transaction — the blockhash or quote may be stale.
Updated 4 days ago