# @veilo/sdk-core

Build private payments and swaps on Solana with Veilo.

The SDK gives TypeScript applications a safe, typed way to:

- shield SOL or SPL tokens into Veilo;
- withdraw, privately transfer, or privately swap shielded funds;
- open and manage private positions, Jupiter perps, and predictions;
- work with Veilo's relayer without handling its encryption protocol yourself;
- add partner-powered private sends and swaps through the Cloak API.

It works with both ESM and CommonJS and includes TypeScript declarations.

> **Project status:** Active development. Test integrations on Devnet before using
> them with real funds.

## Install

```bash
npm install @veilo/sdk-core
```

Install `snarkjs` only if your application generates proofs locally:

```bash
npm install snarkjs
```

You will also need:

- a Solana RPC connection;
- a connected wallet when a user must sign;
- the Veilo circuit `.wasm` and `.zkey` files for local proof generation;
- an API key only for partner services such as Cloak or Jupiter Predictions.

## Choose an integration

| What you want to build | Start with | Who signs? |
| --- | --- | --- |
| Let any wallet shield funds into Veilo | `shield()` | The user's wallet |
| Add private transfers or swaps to a partner app | `VeiloCloakClient` | The user's wallet |
| Build a full Veilo wallet experience | Managed relayer helpers | The relayer for private spends |
| Manage proofs, notes, trees, and transactions yourself | Transaction helpers | Your application or relayer |

For most third-party deposit integrations, `shield()` is the best starting
point. For a managed private send or swap flow, use Cloak.

## Quick start: shield funds

`shield()` creates an unsigned transaction. Your application asks the user's
wallet to sign it, submits it, and then finalizes the new private note.

```ts
import { Connection } from "@solana/web3.js";
import {
  createTransactionProver,
  finalizeShield,
  resolveShieldOwner,
  shield,
} from "@veilo/sdk-core";

const connection = new Connection(process.env.SOLANA_RPC_URL!, "confirmed");

// `wallet` is any connected Solana wallet with `publicKey` and
// `signTransaction` methods.
const owner = await resolveShieldOwner({ username: "alice" });
const prover = createTransactionProver({
  wasmPath: "/circuits/transaction.wasm",
  zkeyPath: "/circuits/transaction_final.zkey",
});

const result = await shield({
  connection,
  amount: 1_000_000_000n, // 1 SOL in lamports
  owner,
  signer: { publicKey: wallet.publicKey },
  prover,
});

const signed = await wallet.signTransaction(result.transaction);
const signature = await connection.sendRawTransaction(signed.serialize());

const confirmation = await connection.confirmTransaction(
  {
    signature,
    blockhash: result.blockhash,
    lastValidBlockHeight: result.lastValidBlockHeight,
  },
  "confirmed",
);

if (confirmation.value.err) {
  throw new Error("Shield transaction failed");
}

const finalized = await finalizeShield({
  connection,
  signature,
  note: result.note,
  owner,
});

console.log("Private note created at leaf", finalized.leafIndex);
```

To shield an SPL token, pass its mint:

```ts
const result = await shield({
  connection,
  mint: usdcMint,
  amount: 5_000_000n, // 5 USDC when the mint has 6 decimals
  owner,
  signer: { publicKey: wallet.publicKey },
  prover,
});
```

Keep `result.note` private. It contains the information needed to recover the
shielded funds. `finalizeShield()` gives you the authoritative leaf index after
the transaction lands.

If the blockhash expires before submission, reuse the proof with `rebuild()`:

```ts
import { rebuild } from "@veilo/sdk-core";

const refreshed = await rebuild(result, { connection });
```

A stale Merkle root needs a new call to `shield()` because it requires a new
proof.

### Transaction v1 shields

`buildShieldV1(result, opts)` turns the same `shield()` / `rebuild()` result
into unsigned transaction v1 bytes (every account inline, no lookup table, the
compute budget in the header) without re-proving. Sign them with `signV1Bytes`
from `@veilo/sdk-core/tx-v1` or a wallet that signs version 1.

v1 has no implicit budget, so the resource limits matter:

- `limits` you pass must be measured for that exact instruction set. SOL and SPL
  shields load different accounts, so their limits differ — do not reuse limits
  measured for one mint on another. A transaction that exceeds its header
  limits fails on-chain and still pays its fee.
- Without `limits`, they are measured by simulation with
  `estimateV1ResourceLimits`, which POSTs to `connection.rpcEndpoint` with a
  plain `fetch` (no custom headers — the Connection's `httpHeaders`, `fetch` and
  `fetchMiddleware` are not used). If your RPC authenticates via custom headers,
  measure through your own transport and pass explicit `limits`.

## Cloak Partner API

Cloak is the simplest route for partner applications that want a private send
or cross-asset private swap. The SDK handles request types, API authentication,
transaction decoding, polling, and structured errors. It never signs for the
user.

Keep your Cloak API key on a trusted server. Do not include it in a browser or
mobile bundle.

### Create an order on your server

```ts
import { VeiloCloakClient } from "@veilo/sdk-core/cloak";

const cloak = new VeiloCloakClient({
  apiKey: process.env.VEILO_CLOAK_KEY!,
});

// The production API defaults to https://api.veilo.network/.
const { quote } = await cloak.getQuote({
  amount: "100000000", // raw token units
  sourceAssetId: "veilo-usdc",
  destinationAssetId: "veilo-usdc",
  senderAddress,
  recipientAddress,
  mode: "exact_in",
});

const { order } = await cloak.createOrder(
  {
    quote, // return the complete quote without changing it
    senderAddress,
    recipientAddress,
  },
  { idempotencyKey: crypto.randomUUID() },
);
```

For a cross-asset swap, use a different `destinationAssetId` and optionally add
`slippageBps` to the quote request. Use `listTokens()` to discover supported
asset IDs and their current limits.

You can override the API address for another environment. Both the origin and
the full Cloak path are accepted:

```ts
new VeiloCloakClient({
  baseUrl: "https://api.veilo.network/", // `/cloak/v1` also works here
  apiKey: process.env.VEILO_CLOAK_KEY!,
});
```

### Sign in the user's application

The order's `transaction` is an unsigned base64 deposit with the sender as the
only signer. It is **Solana transaction v1 by default** (every account inline,
no lookup table, compute limits and priority fee in the message header);
`order.transactionVersion` says what was built, and `txVersion: 0` in
`createOrder` (or `refreshTransaction(id, { txVersion: 0 })`) requests v0.

Sign the raw bytes, which works for both versions. With a Wallet Standard
wallet, check that it advertises transaction version 1 (typed from
`@solana/wallet-standard-features` 1.5.0) and fall back to `txVersion: 0` when
it does not:

```ts
import { decodeCloakTransaction } from "@veilo/sdk-core/cloak";

const feature = wallet.features["solana:signTransaction"];
if (order.transactionVersion === 1 && !feature.supportedTransactionVersions.includes(1)) {
  // Ask your server for `cloak.refreshTransaction(order.trackingId, { txVersion: 0 })`
  // and sign that order instead; the API key never belongs in the browser.
}
const [{ signedTransaction }] = await feature.signTransaction({
  account,
  chain: "solana:mainnet",
  transaction: decodeCloakTransaction(order), // Uint8Array, v0 or v1
});
// sendRawTransaction only base64-encodes the bytes, so it is safe for v1.
const signature = await connection.sendRawTransaction(signedTransaction);
await connection.confirmTransaction(signature, "confirmed");
```

With a local keypair, `signCloakTransaction(order, keypair)` returns signed
bytes for either version. A detached signer (KMS/HSM) signs
`getCloakTransactionMessage(order)` and inserts the result with
`addCloakTransactionSignature(order, publicKey, signature)`, which verifies it
first.

web3.js 1.x cannot sign a v1 transaction: 1.99 decodes it but
`VersionedTransaction.sign()` / `serialize()` throw, and 1.98 and earlier
cannot decode it at all. If your wallet layer signs web3.js objects, create the
order with `txVersion: 0` and keep using `deserializeCloakTransaction`, which
returns a `VersionedTransaction` for v0 and throws for v1:

```ts
const { order } = await cloak.createOrder({ quote, senderAddress, recipientAddress, txVersion: 0 });
const transaction = deserializeCloakTransaction(order); // v0 only
const signed = await wallet.signTransaction(transaction);
const signature = await connection.sendRawTransaction(signed.serialize());
```

Always check `order.transactionVersion` (or `getCloakTransactionVersion`) before
choosing a path: a v1 request can come back as v0 when Cloak falls back.

Send the signature back to your server. The deposit notification is optional,
but it can reduce processing latency:

```ts
await cloak.notifyDeposit({
  trackingId: order.trackingId,
  txSignature: signature,
});

const finalStatus = await cloak.waitForFinalStatus(order.trackingId, {
  intervalMs: 3_000,
  timeoutMs: 20 * 60_000,
});

console.log(finalStatus.status);
```

If an unfunded order's transaction expires, call
`refreshTransaction(order.trackingId)` and ask the user to sign the refreshed
transaction. The refresh keeps the version the order was created with unless
you pass `{ txVersion }`.

## Full private wallet flows

The SDK supports all Veilo privacy-pool operations. Pick the highest-level API
that fits your application:

| Operation | Managed relayer call | Direct SDK helper |
| --- | --- | --- |
| Deposit | Not required | `shield()` or `deposit()` |
| Withdraw | `submitWithdraw()` | `withdraw()` |
| Private transfer | `submitPrivateTransfer()` | `privateTransfer()` |
| Private swap | `submitPrivateSwap()` | `buildPrivateSwapInstructions()` or `transactSwap()` |

### Use the managed relayer helpers

The SDK owns Veilo's production relayer URL, encryption key, request encryption,
and safe retry behavior. Applications call typed functions directly; there is
no relayer client or URL to configure.

```ts
import {
  submitWithdraw,
  type WithdrawRequest,
} from "@veilo/sdk-core/relayer";

async function submitWithdrawal(request: WithdrawRequest) {
  return submitWithdraw(request);
}
```

The same focused entry point provides account authentication, encrypted note
storage, Merkle tree reads, private transfers, private swaps, and these product
flows:

| Product | Relayer helpers |
| --- | --- |
| Private positions | `submitOpenPosition()`, `submitClosePosition()`, `submitMergePositions()` |
| Jupiter perps | `submitJperpOpen()`, `submitJperpClose()`, TP/SL, cancellation, recovery, and reissue helpers |
| Jupiter predictions | `submitPredictionOpen()` and `submitPredictionReissue()` |

#### Submission always targets Veilo's relayer

Every helper above submits to Veilo's relayer. There is no `baseUrl` option, no
client to construct, and no supported way to redirect submission. That is
deliberate, for two independent reasons:

1. **The program requires it.** The privacy pool checks the submitting relayer
   against an on-chain allowlist on every spend. Deposits (`public_amount > 0`)
   are permissionless — which is why `shield()` and `deposit()` work with any
   wallet — but withdrawals, transfers, swaps, positions, perps, and predictions
   all fail with `RelayerNotAllowed` unless the transaction is signed by a
   whitelisted relayer. Pointing the SDK at a different host would produce a
   well-formed request that cannot land on chain.
2. **These requests carry spending keys.** `TransactNote.privateKey`, and the
   `claimantSecretKey` on position close and merge, *are* the spend authority for
   the funds involved (see [A private note is valuable secret
   data](#a-private-note-is-valuable-secret-data)). A configurable submission
   target would be a configurable destination for user funds.

Running your own relayer is a whitelisting conversation rather than a
configuration flag — see [Support](#support). Reaching past the package's
`exports` map to import internal modules is unsupported and not covered by
semver.

### Finding your notes on the public feed

Every row in the compact feed is ciphertext plus a one-byte view tag; nothing on
it says who a note belongs to. You find yours by trial decryption, and the view
tag makes that cheap — it rejects roughly 255 of every 256 foreign rows with a
single hash instead of a full decrypt.

```ts
import { scanCompactNotes, fetchNotesByCommitment } from "@veilo/sdk-core";

const { notes, scanned, nextCursor } = await scanCompactNotes(walletSecretKey, {
  onPage: ({ matched }) => console.log(`${matched} found so far`),
});
```

It paginates to exhaustion by default and derives the X25519 key once for the
whole scan. Stop early with `maxPages` or by returning `false` from `onPage`;
`nextCursor` is then non-null and can be passed back later to resume. The feed
carries only unspent, unclaimed notes, and it needs no auth token — scanning
reveals nothing without your key.

`fetchNotesByCommitment(commitments)` looks up known commitments instead,
chunked at the server's 50-per-request cap and issued concurrently. Commitments
the relayer does not know are simply absent from the result.

### Consolidating notes before a large spend

When no one or two notes cover an amount, `selectNotesForAmount` reports
`requiresMerge`. `planNoteConsolidation` says what to do about it:

```ts
const plan = planNoteConsolidation(unspentNotes, amountRaw, { mint });
if (plan.ok) {
  for (const { inputs, outputAmount } of plan.steps) {
    // one 2-in-1-out privateTransfer to yourself per step
  }
}
```

Each step is a self-transfer combining two inputs into one, so k notes need k-2
steps before a final two-input spend. Steps chain — a later step can consume an
earlier step's output, and inputs are tagged `{ kind: "note" }` or
`{ kind: "step", step }` so you always know which. The two smallest are merged
each round, which retires dust first and leaves large notes intact. The plan is
pure: it computes, it does not execute.

### Spend status and private balance

Nothing in a note says whether it has been spent — a note is yours until its
nullifier is published. So a balance is always two steps, and the SDK gives you
both:

```ts
import { getPrivateBalance, checkNullifiersSpent } from "@veilo/sdk-core";

const { total, byMint, unspent, spent } = await getPrivateBalance(myNotes);
```

Each note needs an `amount`, a 64-character hex `nullifier`, and optionally a
`mint` to group by. `getPrivateBalance` asks the relayer which nullifiers are
spent, then sums what is left; `unspent` and `spent` hand back your own note
objects, so you can render from them directly.

`checkNullifiersSpent(nullifiers)` is the same question on its own. It
deduplicates, splits into the server's 200-per-request batches, issues them
concurrently, and returns a `Set` for membership testing. It also lower-cases
input: the endpoint validates case-insensitively but answers in lower case, so
upper-case hex would otherwise never match and every note would read as unspent.

To compute a balance without the relayer — from a chain scan of nullifier
events, or a local cache — supply the answer yourself:

```ts
await getPrivateBalance(myNotes, { resolveSpent: async (ns) => mySpentSet(ns) });
```

Amounts stay `bigint` end to end. A `number` amount above `Number.MAX_SAFE_INTEGER`
is rejected rather than silently rounded, and a malformed nullifier throws
instead of counting as unspent — either would overstate a balance.

### Choosing notes to spend

Two rules govern this, and both are easy to get wrong:

- the transaction circuit is **2-in-2-out**, so one transaction spends at most
  two notes;
- notes are only co-spendable inside the **same Merkle tree**, and the tree is
  derived from the mint.

```ts
import { selectNotesForAmount, canonicalTreeId } from "@veilo/sdk-core";

const selection = selectNotesForAmount(unspentNotes, amountRaw, { mint });
if (!selection.ok) throw new Error(selection.message); // NO_NOTES | INSUFFICIENT_FUNDS
if (selection.requiresMerge) {
  // more than two inputs — combine them before proving
}
```

It searches each tree independently and returns the best result: the smallest
single note that covers the amount, else the pair with the least change (found
by a two-pointer scan, not an all-pairs search), else the largest notes greedily
with `requiresMerge` set. A failure reports `available`, the largest total
reachable within one tree.

`canonicalTreeId(mint)` derives a note's tree the way the relayer does. **Derive
it; never trust a stored `treeId`.** Change notes are written with the on-chain
shard index while deposits and synced notes carry the mint-derived id — for
native SOL both are 0 so the difference hides, but for SPL tokens they diverge
and co-spending across the two fails at proof time as a commitment mismatch.

### Private product keys and recovery

Private positions, perps, and predictions use deterministic client-held keys.
The SDK provides the exact derivations used by Veilo's wallet, extension,
relayer, and program tests. Keep the spending key and claimant secret on the
user's device; send claimant secrets only inside the SDK's encrypted relayer
requests.

```ts
import {
  derivePositionKeyBundle,
  deriveSpendingKeyWithSigner,
  encodeClaimantSecretKey,
  initPoseidon,
  submitOpenPosition,
} from "@veilo/sdk-core";

await initPoseidon();
const spendingKey = await deriveSpendingKeyWithSigner(
  (message) => wallet.signMessage(message),
);
const positionKeys = derivePositionKeyBundle(spendingKey, nextPositionIndex);

await submitOpenPosition({
  // Select private source notes in your wallet before calling the relayer.
  notes,
  sourceMintAddress,
  destMintAddress,
  swapAmountRaw,
  slippageBps: 50,
  userPublicKey: wallet.publicKey.toBase58(),
  veiloPublicKey,
  position: positionKeys.position,
});

// Persist the returned position data and `nextPositionIndex`. Re-derive the
// same claimant later when closing or merging the position.
const claimantSecretKey = encodeClaimantSecretKey(positionKeys.claimant);
```

Use `deriveJperpKeyBundle()` and `getJperpMarketPayload()` for private perps.
Use `derivePredictionKeyBundle()` for private predictions. Lower-level key
derivations are also exported when an application needs them individually. The
PDA helpers in each focused entry point support on-chain recovery scans on a
fresh device.

Predictions have one additional step: Jupiter returns an unsigned order,
close, or claim transaction. The SDK exposes that API without taking custody
of signing:

```ts
import {
  JupiterPredictionClient,
  deserializePredictionTransaction,
} from "@veilo/sdk-core/predictions";

const predictions = new JupiterPredictionClient({ apiKey: jupiterApiKey });
const order = await predictions.placeOrder({
  ownerPubkey: ephemeral.publicKey.toBase58(),
  marketId,
  isYes: true,
  isBuy: true,
  depositAmount: "5000000",
  depositMint: usdcMint,
});
const transaction = deserializePredictionTransaction(order);
// Ask the deterministic ephemeral wallet to sign, then submit and confirm it.
```

The SDK deliberately does not own application storage or choose which notes to
spend. Persist indexes and returned product records only after the corresponding
transaction is confirmed.

### Use the direct helpers

Direct helpers are intended for applications that already maintain Veilo note
and Merkle tree state and can generate proofs.

```ts
import {
  NATIVE_SOL_MINT,
  createTransactionProver,
  createVeiloProgram,
  deposit,
} from "@veilo/sdk-core";

const program = createVeiloProgram(connection, anchorWallet);
const proofBuilder = createTransactionProver({
  wasmPath: "/circuits/transaction.wasm",
  zkeyPath: "/circuits/transaction_final.zkey",
});

const built = await deposit({
  program,
  depositor: { publicKey: wallet.publicKey },
  mintAddress: NATIVE_SOL_MINT,
  amount: 1_000_000_000n,
  recipientPubkey: veiloOwnerPublicKey,
  noteRecipientWallet: wallet.publicKey,
  tree,
  proofBuilder,
  treeId: 0,
});

const signed = await wallet.signTransaction(built.transaction);
const signature = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction(signature, "confirmed");

// Update the local tree only after on-chain confirmation.
const receipt = built.commit();
```

`withdraw()` and `privateTransfer()` accept two spendable input notes, build the
proof, submit through the supplied relayer signer, and return the resulting
change or output notes. Important rules are enforced by the SDK:

- withdrawal amounts must be positive and fit within the selected notes;
- private-transfer outputs must equal the total input amount;
- direct private transfers use a zero public fee;
- note-recipient wallet keys should be supplied so new notes remain recoverable.

Private swaps normally use Jupiter versioned transactions and address lookup
tables. Use `buildPrivateSwapInstructions()` to add Veilo's atomic instructions
to the exact Jupiter route. Preserve Jupiter's account order and duplicate
accounts. `transactSwap()` is available when a legacy transaction is sufficient.

The low-level `transact()` and `buildRawTransactInstruction()` exports are for
custom transaction composition. Most applications should use the helpers above.

## Important concepts

### Amounts use base units

Amounts are `bigint` values or decimal strings in the token's smallest unit:

```ts
import { sol } from "@veilo/sdk-core/config";

const oneSol = sol(1);       // 1_000_000_000n lamports
const fiveUsdc = 5_000_000n; // 5 USDC for a 6-decimal mint
```

Avoid JavaScript floating-point values for token arithmetic.

### A private note is valuable secret data

A note contains the information needed to locate and spend private funds. Store
it encrypted, never log it, and never send its unencrypted contents to an
analytics or application server.

For shielding, prefer `resolveShieldOwner()`. It obtains the spending and note
viewing keys as a matched pair. Supplying unrelated keys can create a note that
the recipient cannot discover.

### Confirmation comes before local state updates

Do not mark notes as spent, insert commitments into a local tree, or persist a
predicted leaf index until the transaction is confirmed. Use `finalizeShield()`
for shields and call a deposit's `commit()` callback only after confirmation.

### Proof files are separate

Circuit `.wasm` and `.zkey` files are intentionally not bundled with the npm
package because they are large — `files` ships `dist/` only. Provide local
paths, byte arrays, or hosted URLs to `createTransactionProver()` and
`createSwapProver()`.

Both arguments are optional. Omit them and the prover resolves artifacts from
disk on first use, first hit wins:

1. `$VEILO_CIRCUITS_DIR`
2. `<package>/circuits` — populated by `npm run copy:circuits`
3. `<cwd>/circuits`
4. `<cwd>/node_modules/@veilo/sdk-core/circuits`

```ts
const prover = createTransactionProver();            // resolved from disk
const prover = createTransactionProver({ wasmPath, zkeyPath }); // explicit
```

`resolveCircuitArtifacts("transaction" | "swap")` performs that lookup on its
own if you want the paths. Resolution is **Node-only and lazy** — it imports
`node:fs` inside the call so browser bundles can still import this module, and
there is nothing to resolve in a browser anyway. Browser builds must pass
`CircuitArtifacts` explicitly. A miss throws with every path it tried.

## Error handling

Shield and Cloak errors include stable fields that applications can use to show
useful recovery actions.

```ts
import {
  CloakApiError,
  mapShieldError,
} from "@veilo/sdk-core";

try {
  // Build or submit a Veilo operation.
} catch (error) {
  if (error instanceof CloakApiError) {
    console.error(error.code, error.status, error.message);
  } else {
    const shieldError = mapShieldError(error);
    console.error(shieldError.code, shieldError.message);
    // retryable is `rebuild`, `reshield`, or `none`.
    console.log(shieldError.retryable);
  }
}
```

When handling Cloak separately, `CloakApiError` also exposes actionable values
such as `min`, `max`, `field`, `maxBps`, and `retryAfter` when the API returns
them.

## Package entry points

Import from the package root for convenience or use a focused entry point:

```ts
import { shield } from "@veilo/sdk-core";
import { getPoolPdas } from "@veilo/sdk-core/accounts";
import { createUTXO } from "@veilo/sdk-core/notes";
import { deposit, withdraw } from "@veilo/sdk-core/transactions";
import { submitWithdraw } from "@veilo/sdk-core/relayer";
import { VeiloCloakClient } from "@veilo/sdk-core/cloak";
import { derivePositionKeyBundle } from "@veilo/sdk-core/positions";
import { getJperpMarketPayload } from "@veilo/sdk-core/perps";
import { JupiterPredictionClient } from "@veilo/sdk-core/predictions";
```

Available focused entry points:

- `@veilo/sdk-core/accounts`
- `@veilo/sdk-core/cloak`
- `@veilo/sdk-core/config`
- `@veilo/sdk-core/idl`
- `@veilo/sdk-core/identity`
- `@veilo/sdk-core/notes`
- `@veilo/sdk-core/perps`
- `@veilo/sdk-core/positions`
- `@veilo/sdk-core/predictions`
- `@veilo/sdk-core/poseidon`
- `@veilo/sdk-core/proof`
- `@veilo/sdk-core/prover`
- `@veilo/sdk-core/relayer`
- `@veilo/sdk-core/shield`
- `@veilo/sdk-core/transactions`
- `@veilo/sdk-core/tx-v1`

## Advanced building blocks

The root package also exports tools for teams building their own Veilo wallet
or relayer:

- account and PDA queries;
- UTXO creation, ownership, encryption, and recovery;
- Merkle tree reconstruction from on-chain events;
- transaction and swap witness preparation;
- Poseidon hashing helpers;
- pool initialization and administration;
- raw IDL-correct transaction instruction builders.

These APIs are fully typed. Use their TypeScript definitions as the source of
truth for required inputs.

### Historical IDLs and commitment recovery

The SDK includes the current, legacy, and legacy-2 privacy-pool IDLs used by the
relayer. Event scans automatically try the historical layouts, so applications
can rebuild trees across program upgrades.

```ts
import {
  PRIVACY_POOL_IDLS,
  buildTreeFromEvents,
  resolveCommitmentEvents,
} from "@veilo/sdk-core";
```

`buildTreeFromEvents()` includes Veilo's protected historical repairs by
default and rejects gaps that would produce an incorrect root. When a confirmed
instruction appended commitments without emitting `CommitmentEvent`, use
`resolveCommitmentEvents()` with the expected commitments and current local
tree. It prefers emitted metadata and computes only the missing entries.

For the deployed eventless `open_position` path, use
`recoverEventlessOpenPositionEvents()`. It decodes both commitments from the
instruction and reads the authoritative global position-tree leaf index from
the program-owned PositionPDA. `buildGlobalPositionTreeFromEvents()` mirrors
the relayer's all-mint position-tree reconstruction.

## Development

```bash
# Build CommonJS, ESM, and declaration outputs
npm run build

# Run deterministic unit and proof tests
npm test

# Run opt-in live Devnet integration tests
npm run test:integration

# Check module boundaries for dependency cycles
npm run check:cycles
```

## Support

- [Repository](https://github.com/VeiloSolana/veilo-sdk)
- [Issue tracker](https://github.com/VeiloSolana/veilo-sdk/issues)

When reporting a problem, include the SDK version, runtime, Solana cluster, and
the error's `code` and `status` or `programCode` where available. Never include
private notes, wallet secret keys, API keys, or authentication tokens.

## License

[ISC](./LICENSE)
