> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hadron.fi/llms.txt
> Use this file to discover all available pages before exploring further.

# Curve Management

> Queue curve point edits with submitCurveUpdates for 58 CU alongside your oracle updates, validate them in simulation, and let the next swap apply them.

`setCurve` rewrites an entire curve slot and validates every segment of it in the same transaction. For smooth curves this can cost tens of thousands of compute units, all in your update transaction.

Queued curve updates split that work in two:

* **Your transaction** only writes the edits into a small buffer. `submitCurveUpdates` costs **58 CU** regardless of batch size.
* **The validation work** runs when the batch is applied, which by default is the **next swap** on the pool.

A smaller compute-unit limit means a cheaper priority fee for the same price per CU, and a transaction that lands more reliably when blocks are busy. That makes queued updates a good fit for operators who update the curve alongside every oracle tick.

Three instructions make up this flow:

| **Instruction** | **What it does** | **Accounts** |
| :- | :- | :- |
| `submitCurveUpdates` | Writes a batch of up to 28 point edits into the pool's `CurveUpdates` buffer. Nothing on the curve changes yet. | authority, curveUpdates |
| `validateCurveUpdates` | Dry-runs the queued batch against the live curve, then **always reverts**. Use it only in simulation. | authority, curveMeta, curvePrefabs, curveUpdates |
| `applyCurveUpdates` | Applies the queued batch to the curve and clears the buffer. | authority, curveMeta, curvePrefabs, curveUpdates |

<Warning>
  Because the next swap applies the queued batch, an invalid batch makes **every swap on the pool fail** with that batch's error until you replace it. Always validate a batch with `validateCurveUpdates` before you send it.
</Warning>

***

## The recommended flow

1. Build the `submitCurveUpdates` instruction.
2. **Simulate** a transaction containing `submitCurveUpdates` followed by `validateCurveUpdates`.
   * Error `73` (`ValidationSimulationSuccess`) on the validate instruction means the batch is valid.
   * Any other error is the exact error the real apply would return.
3. **Remove** `validateCurveUpdates` and send `submitCurveUpdates`, typically in the same transaction as your `updateMidprice`. The next swap applies the batch.

`validateCurveUpdates` never commits state, so a transaction that contains it always fails. Include it only when simulating.

```mermaid theme={null}
flowchart LR
    A[Build submitCurveUpdates] --> B["simulate: submit + validate"]
    B -->|"error 73"| C["send: updateMidprice + submit"]
    B -->|"any other error"| D[Fix the ops]
    D --> A
    C --> E["next swap applies the batch"]
```

***

## submitCurveUpdates

```typescript theme={null}
import {
  Hadron,
  CurveType,
  CurveUpdateOpKind,
  Interpolation,
  decodeCurveUpdates,
  toQ32,
} from "@hadron-fi/sdk-v2";

const pool = await Hadron.load(connection, poolAddress);

// Read the live curve so each op can pin the point it expects to edit.
const bid = pool.getActiveCurves().priceBid;
const slot = pool.getActiveCurveSlots().priceBid;

// `sequence` must be >= the buffer's current sequence.
const updatesInfo = await connection.getAccountInfo(pool.addresses.curveUpdates);
const { sequence } = decodeCurveUpdates(updatesInfo!.data);

const submitIx = pool.submitCurveUpdates(authority.publicKey, sequence + 1n, [
  {
    curveType: CurveType.PriceBid,
    opKind: CurveUpdateOpKind.Edit,
    pointIndex: 3,
    interpolation: Interpolation.Linear,
    amountIn: bid.points[3].amountIn,     // keep the same x, move the price
    priceFactorQ32: toQ32(0.9975),
    params: new Uint8Array(4),
    targetSlot: slot,                      // the active PriceBid slot
    expectedXIn: bid.points[3].amountIn,  // current x_in at pointIndex
  },
]);
```

| **Field** | **Type** | **Description** |
| :- | :- | :- |
| `sequence` | `bigint` | Must be ≥ the buffer's current sequence. Protects against out-of-order submissions. |
| `curveType` | `CurveType` | `PriceBid`, `PriceAsk`, `RiskBid` or `RiskAsk` |
| `opKind` | `CurveUpdateOpKind` | `Edit` overwrites the point at `pointIndex`. `Add` inserts before it. `Remove` deletes it. |
| `pointIndex` | `number` | Index of the point to edit, insert before, or remove |
| `interpolation` | `Interpolation` | Interpolation for the edited or added point |
| `amountIn` | `bigint` | The point's x value, in the same units as the curve's x mode |
| `priceFactorQ32` | `bigint` | New price (or risk) factor in Q32. Use `toQ32(n)`. |
| `params` | `Uint8Array(4)` | Interpolation parameters, for example the Hyperbolic `k` |
| `targetSlot` | `number` | The slot that was active when you built the op. The op fails with `CurveUpdateSlotMismatch` (67) if the active slot has changed. |
| `expectedXIn` | `bigint` | The x value currently at `pointIndex`. The op fails with `CurveUpdateExpectedXMismatch` (72) if the curve has moved. Ignored for `Add`. |

<Note>
  Each submit **replaces** the buffer; it does not append. If you submit twice before the buffer is applied, only the second batch remains.
</Note>

***

## validateCurveUpdates

Simulate `submitCurveUpdates` and `validateCurveUpdates` together, so the dry-run checks the batch you are about to send:

```typescript theme={null}
import {
  TransactionMessage,
  VersionedTransaction,
} from "@solana/web3.js";
import { getHadronErrorMessage } from "@hadron-fi/sdk-v2";

const VALIDATION_SIMULATION_SUCCESS = 73;

const validateIx = pool.validateCurveUpdates(authority.publicKey);

const { blockhash } = await connection.getLatestBlockhash();
const simTx = new VersionedTransaction(
  new TransactionMessage({
    payerKey: authority.publicKey,
    recentBlockhash: blockhash,
    instructions: [submitIx, validateIx],
  }).compileToV0Message()
);

const sim = await connection.simulateTransaction(simTx, { sigVerify: false });
const err = sim.value.err as { InstructionError?: [number, { Custom?: number } | string] } | null;
const code =
  err?.InstructionError && typeof err.InstructionError[1] === "object"
    ? err.InstructionError[1].Custom
    : undefined;

if (code !== VALIDATION_SIMULATION_SUCCESS) {
  throw new Error(
    `Curve update rejected: ${code !== undefined ? getHadronErrorMessage(code) : JSON.stringify(sim.value.err)}`
  );
}
```

On failure the simulation logs name the op that failed, for example `L-06 apply[2] ct=0 kind=Edit idx=4 fail=point-op-validation`.

<Tip>
  You can also call `validateCurveUpdates` on its own to check a batch that is already queued on-chain.
</Tip>

***

## Send the update

<Danger>
  **Deferred validation can halt swaps on your pool.** `submitCurveUpdates` does not check your ops. They are only checked when the batch is applied, which is usually inside the next swap. If the batch is invalid, **every swap on the pool fails** until you submit a valid batch to replace it.

  This is the tradeoff for keeping validation out of your update transaction. Never send a batch without first simulating it with `validateCurveUpdates`. If you cannot simulate first, use `setCurve`, which rejects a bad curve in your own transaction.
</Danger>

After the simulation passes, remove `validateCurveUpdates` and send `submitCurveUpdates` with your oracle update under a tight compute-unit limit:

```typescript theme={null}
import { ComputeBudgetProgram } from "@solana/web3.js";

const midIx = pool.updateMidprice(authority.publicKey, { midpriceQ32: toQ32(142.35) });

const { blockhash: sendBlockhash } = await connection.getLatestBlockhash();
const tx = new VersionedTransaction(
  new TransactionMessage({
    payerKey: authority.publicKey,
    recentBlockhash: sendBlockhash,
    instructions: [
      ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000 }),
      ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 50_000 }),
      midIx,
      submitIx, // validateIx removed
    ],
  }).compileToV0Message()
);
tx.sign([authority]);
await connection.sendTransaction(tx);
```

The Hadron instructions in this transaction use under 100 CU (`updateMidprice` about 37, `submitCurveUpdates` 58). Each compute-budget instruction adds about 150 CU, so a limit of 1,000 leaves plenty of headroom.

<Note>
  Until the next swap applies the batch, the curve account still holds the old points, and off-chain quoters (including aggregators) quote from it. The swap that applies the batch executes against the new points. Send `applyCurveUpdates` yourself if you need the new points live and quoted before the next trade.
</Note>

***

## applyCurveUpdates

```typescript theme={null}
const ix = pool.applyCurveUpdates(authority.publicKey);
```

Applies every queued op in order and clears the buffer, exactly as the next swap would. If any op fails, the whole instruction fails and nothing changes. Applying an empty buffer is a no-op. Use it when you want the edits live immediately and are willing to pay the validation CU yourself.

***

## Batch rules

* **Ops apply in order.** Each op is checked against the curve as the earlier ops in the same batch left it, not against the final curve. A set of edits that is valid in the end can still fail midway. For example, to move two neighbouring points to the right, edit the right-hand point first. If you edit the left-hand point first, it lands past its old neighbour and the op fails with `CurvePointsNotSorted` (error `9`). Validate catches this, so reorder the ops and simulate again.
* **Indexes follow earlier ops.** `pointIndex` and `expectedXIn` refer to the curve after the earlier ops in the batch. After an `Add` or `Remove`, the points to its right shift by one.
* **One failure fails the batch.** No partial apply.
* **Max 28 ops** per submit.

***

## Where the compute units go

Validation cost depends on the curve's **interpolation** and on **how many segments are checked**. `setCurve` checks every segment. A queued op checks only the segments next to the point it touches (at most two). Measured on the deployed program:

| **Curve** | **`setCurve`, paid by you** | **1 queued op, paid at apply** |
| :- | -: | -: |
| Flat Step, 2 points | 667 | 678 |
| MarginalStep, 5 points | 1,535 | 1,003 |
| MarginalStep, 21 points | 5,407 | 1,003 |
| Linear, 2 points | 3,151 | 3,167 |
| Linear, 5 points | 10,903 | 5,687 |
| Linear, 21 points | 52,247 | 5,687 |

With the queue, your transaction pays only the 58 CU for `submitCurveUpdates`. The right-hand column is paid by whoever applies the batch: the next swap, or you if you send `applyCurveUpdates`. `validateCurveUpdates` costs the same as an apply, but only in simulation.

* **Updating the curve with every oracle tick:** use the queue. Your update transaction stays under 100 CU of Hadron work however long or smooth the curve is.
* **Short or cheap curves:** `setCurve` on a 2-point flat curve already costs under 700 CU, so the queue saves little.
* **Keep batches small.** The swap that applies the batch pays about 1,000–5,700 CU per op on top of its own cost. A large batch can push an aggregator's swap over the compute budget it simulated. For large reshapes, use `setCurve` or send `applyCurveUpdates` yourself.

***

## Errors

| **Code** | **Name** | **Meaning** |
| :- | :- | :- |
| 9 | `CurvePointsNotSorted` | The op would leave x values out of order |
| 61 | `ConcavityViolation` | The op would make the curve non-concave |
| 62 | `PriceFactorTooLarge` | Price factor above 1.0 (`Q32_ONE`) |
| 67 | `CurveUpdateSlotMismatch` | `targetSlot` is not the active slot for that curve type |
| 72 | `CurveUpdateExpectedXMismatch` | `expectedXIn` does not match the point at `pointIndex` |
| 73 | `ValidationSimulationSuccess` | `validateCurveUpdates` dry-run passed (intentional revert) |

See [Errors](/sdk/errors) for the full list.
