oculi 0.1.0

Structured, Borsh-serialized Anchor events as a drop-in replacement for msg! in Solana programs.
Documentation
# Oculi

[![crates.io](https://img.shields.io/crates/v/oculi.svg)](https://crates.io/crates/oculi)
[![docs.rs](https://img.shields.io/docsrs/oculi)](https://docs.rs/oculi)
[![license](https://img.shields.io/crates/l/oculi.svg)](./LICENSE)

**Structured, Borsh-serialized Anchor events — a drop-in replacement for `msg!` that costs ~93% fewer compute units.**

`msg!` formats strings on-chain. Logging a swap (an amount and a pubkey) costs roughly **11,000 CU** per call. Oculi serializes a typed struct into an Anchor event instead — the same data for **~781 CU**:

```text
Standard msg!      11086 CU
Oculi log!           781 CU   (92.96% cheaper)
```

The event lands as a `Program data:` log in the transaction metadata and decodes from any Borsh-aware client (TypeScript, Python, Go). No string formatting, no runtime schema reflection, no dynamic dispatch.

## Install

```toml
[dependencies]
oculi = "0.1"
anchor-lang = "0.30"
```

Tested against `anchor-lang` 0.30.x. MSRV: **Rust 1.75**.

## Quick start

Define a payload struct and log it. The first argument is the event name; the second is any `AnchorSerialize` value.

```rust
use anchor_lang::prelude::*;
use oculi::log_info;

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct SwapExecuted {
    pub amount: u64,
    pub user: Pubkey,
}

pub fn swap(ctx: Context<Swap>, amount: u64) -> Result<()> {
    log_info!("swap_executed", SwapExecuted {
        amount,
        user: ctx.accounts.user.key(),
    });
    Ok(())
}
```

## Log levels

Each level has a convenience macro that wraps the core `log!` with the level pre-set:

| Macro | Level | Typical use |
|-------|-------|-------------|
| `log_debug!(name, payload)` | `Debug` | Verbose internal state snapshots |
| `log_info!(name, payload)`  | `Info`  | Milestones indexers should consume |
| `log_warn!(name, payload)`  | `Warn`  | Recoverable anomalies monitors should alert on |
| `log_error!(name, payload)` | `Error` | A sub-operation failed but the instruction recovered |

For the most verbose `Trace` level, call the core macro directly:

```rust
use oculi::{log, LogLevel};

log!(LogLevel::Trace, "fn_enter", FnEnter { instruction: 1 });
```

## Zero overhead in production: `devnet-only`

Enable the `devnet-only` feature in your mainnet build and every `log!` — including payload serialization — is compiled out to a no-op. The compiler eliminates the entire logging path, so you pay **nothing** for log statements you leave in the code.

```toml
# mainnet / production
oculi = { version = "0.1", features = ["devnet-only"] }

# devnet / test — logging active
oculi = "0.1"
```

| Feature | Default | Effect |
|---------|---------|--------|
| `devnet-only`   | off | Compiles out all `emit!` calls. Enable for mainnet. |
| `no-entrypoint` | off | Forwards to `anchor-lang/no-entrypoint` for CPI dependents. |

## Serialization failures don't panic

If Borsh serialization of a payload ever fails, Oculi emits a sentinel event instead of panicking on-chain:

```text
event_name = "__oculi_serialization_error"
payload    = []
```

The failure stays observable off-chain and no compute budget is wasted unwinding a panic.

## Decoding events off-chain

Each event is Borsh-encoded with an 8-byte Anchor discriminator, then:

```text
[discriminator: 8][level: 1][name_len: 4 LE][name: n][payload_len: 4 LE][payload: m]
```

Decoding in TypeScript:

```typescript
import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";

const logs = txDetails?.meta?.logMessages ?? [];
const dataLog = logs.find((l) => l.includes("Program data: "));
const buffer = Buffer.from(dataLog!.split("Program data: ")[1], "base64");

let offset = 8;                              // skip 8-byte Anchor discriminator
const level = buffer.readUInt8(offset);      offset += 1;
const nameLen = buffer.readUInt32LE(offset); offset += 4;
const eventName = buffer.subarray(offset, offset + nameLen).toString("utf8");
offset += nameLen;
const payloadLen = buffer.readUInt32LE(offset); offset += 4;
const payload = buffer.subarray(offset, offset + payloadLen);

// Decode your struct — e.g. SwapExecuted: u64 (8 bytes LE) + Pubkey (32 bytes)
const amount = new BN(payload.subarray(0, 8), "le").toNumber();
const user   = new PublicKey(payload.subarray(8, 40)).toBase58();
```

The `LogLevel` discriminants are stable: `Trace=0`, `Debug=1`, `Info=2`, `Warn=3`, `Error=4`. They are part of the public API and won't change without a major version bump.

## API

| Export | Kind | Description |
|--------|------|-------------|
| `log!` | macro | Core macro; takes a `LogLevel`, an event name, and a payload |
| `log_debug!` / `log_info!` / `log_warn!` / `log_error!` | macros | Level-specific wrappers over `log!` |
| `LogLevel` | enum | Severity level; `#[repr(u8)]` with stable discriminants |
| `OculiEvent` | struct | The Anchor event type emitted by every `log!` call |

Full documentation: **[docs.rs/oculi](https://docs.rs/oculi)**.

## Contributing

Issues and PRs welcome at [github.com/Oculi-logs/Oculi](https://github.com/Oculi-logs/Oculi). Run `cargo fmt` and `cargo clippy -- -D warnings` before opening a PR — CI enforces both.

## License

MIT — see [LICENSE](./LICENSE).