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:

  1. Validates your parameters
  2. Selects the best pool (for open-by-token) or validates the position exists
  3. Gets a Jupiter swap quote for the token swap
  4. Builds a Solana VersionedTransaction with all necessary instructions
  5. 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

StateMeaning
ONCHAINTransaction confirmed, position detected by listener
EXECUTEDPosition fully synced with on-chain state — all fields populated
CLOSEDClose transaction confirmed
CLOSED_EXECUTEDClose fully synced — final P&L calculated
LIQUIDATEDPosition 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

StepMax Safe Delay
API call to signSeconds (quote is fresh)
Sign to submit<60 seconds (blockhash expiry)
Submit to confirm5-30 seconds (network dependent)
Confirm to position sync1-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.