solignition-cli 2.0.0

CLI tool for deploying Solana programs via the Solignition lending protocol
# CLAUDE.md — Solignition CLI


## Project Overview


Rust CLI for the **Solignition** protocol — a Solana DeFi lending platform that lets developers deploy programs without upfront capital. Users borrow SOL to cover deployment costs (rent + tx fees), the protocol's deployer service deploys their `.so` binary, and upon repayment the program's upgrade authority transfers to the borrower.

## Architecture


```
src/
├── main.rs        # CLI entry point, clap command definitions, command handler functions
├── config.rs      # Config file (~/.solignition/config.toml), keypair loading
├── client.rs      # HTTP client for the deployer API (upload, notify, status)
├── solana_ops.rs  # On-chain interactions: build & send transactions, parse accounts
└── display.rs     # Terminal output formatting, tables, spinners, colors
```

### Key Design Decisions


- **No Anchor client dependency** — instructions are built manually from IDL discriminators to avoid version conflicts and keep the binary small. The discriminators are hardcoded constants extracted from the IDL.
- **Raw account parsing** — on-chain account data (ProtocolConfig, Loan) is parsed from raw bytes using offset constants derived from the Anchor struct layout (8-byte discriminator prefix + fields in declaration order).
- **Async with tokio** but `solana-client::RpcClient` is synchronous — the async boundary is at the command handler level.

## On-Chain Program


- **Program ID**: `HVzpjSxwECnb6uY9Jnia48oJp4xrQiz5jgc5hZC5df63`
- **Framework**: Anchor 0.31
- **IDL source**: `../anchor/target/types/solignition.ts` (TypeScript IDL)
- **IDL JSON**: loaded by the deployer service at runtime

### PDA Seeds


| Account          | Seeds                                          |
|------------------|------------------------------------------------|
| ProtocolConfig   | `"config"`                                     |
| Vault            | `"vault"`                                      |
| AdminPda         | `"admin"`                                      |
| AuthorityPda     | `"authority"`                                  |
| Loan             | `"loan"` + loan_id (u64 LE) + borrower pubkey  |
| DepositorRecord  | `"depositor"` + depositor pubkey               |
| EventAuthority   | `"__event_authority"`                          |

### Instructions Used by CLI


| Instruction    | Discriminator                          | Args                                              |
|----------------|----------------------------------------|----------------------------------------------------|
| `requestLoan`  | `[120, 2, 7, 7, 1, 219, 235, 187]`    | principal: u64, duration: i64, interestRateBps: u16, adminFeeBps: u16 |
| `repayLoan`    | `[224, 93, 144, 77, 61, 17, 137, 54]` | loanId: u64                                        |

### Loan States (enum byte values)


| Value | State                   |
|-------|-------------------------|
| 0     | Active                  |
| 1     | Repaid                  |
| 2     | Recovered               |
| 3     | Pending                 |
| 4     | RepaidPendingTransfer   |
| 5     | Reclaimed               |

### Account Data Layout — ProtocolConfig


```
Offset  Size  Field
0       8     Anchor discriminator
8       32    admin (Pubkey)
40      32    treasury (Pubkey)
72      32    deployer (Pubkey)
104     2     admin_fee_split_bps (u16)
106     2     default_interest_rate_bps (u16)
108     2     default_admin_fee_bps (u16)
110     8     total_loans_outstanding (u64)
118     8     total_shares (u64)
126     8     total_yield_distributed (u64)
134     8     loan_counter (u64)
142     1     is_paused (bool)
143     1     bump (u8)
```

### Account Data Layout — Loan


```
Offset  Size  Field
0       8     Anchor discriminator
8       8     loan_id (u64)
16      32    borrower (Pubkey)
48      32    program_pubkey (Pubkey)
80      8     principal (u64)
88      8     duration (i64)
96      2     interest_rate_bps (u16)
98      2     admin_fee_bps (u16)
100     8     admin_fee_paid (u64)
108     8     start_ts (i64)
116     1     state (enum tag)
117     32    authority_pda (Pubkey)
149+    ...   Optional fields (repaid_ts, recovered_ts, interest_paid, etc.)
```

> ⚠️ **These offsets are derived from the IDL type definitions assuming no padding. If Anchor adds alignment padding on any version change, these need to be verified against actual on-chain data using `solana account <PDA> --output json`.**

## Deployer API (v1)


The CLI communicates with a Node.js/Express deployer service. Base URL is
configured via `api_url`. All non-ops endpoints are versioned under `/v1/`.

The deployer ships an OpenAPI 3.1 spec at `GET /openapi.json` and renders
Swagger UI at `GET /docs` — those are the canonical, always-current
reference. The table below is the same information for quick lookup.

### Auth (`solignition-auth-v1`)


Every authed call sends four headers:

| Header              | Contents                                     |
|---------------------|----------------------------------------------|
| `X-Auth-Pubkey`     | base58 Solana pubkey of the signer           |
| `X-Auth-Timestamp`  | Unix milliseconds (decimal string)           |
| `X-Auth-Nonce`      | base58 of 16 random bytes                    |
| `X-Auth-Signature`  | ed25519 signature, base58-encoded            |

The signed message is the six-line canonical form:

```
solignition-auth-v1
<METHOD>
<PATH_INCLUDING_QUERY>
<TIMESTAMP_MS>
<NONCE_BASE58>
<BODY_HASH_HEX>
```

**Path must include the query string** for list endpoints — the deployer
hashes `req.originalUrl`, so `GET /v1/uploads?borrower=…&limit=…` is
signed with that full path.

### Endpoints Used


| Method | Path                                                       | Purpose                                              | Status |
|--------|------------------------------------------------------------|------------------------------------------------------|--------|
| POST   | `/v1/uploads`                                              | Upload .so binary (multipart)                        | 201    |
| GET    | `/v1/uploads/:fileId`                                      | Get upload info                                      | 200    |
| GET    | `/v1/uploads?borrower=…&status=…&limit=…&offset=…`         | List uploads (paginated envelope)                    | 200    |
| DELETE | `/v1/uploads/:fileId`                                      | Delete a not-yet-deployed upload                     | 204    |
| POST   | `/v1/loans`                                                | Trigger deployment after the borrower signs `requestLoan` | 201 |
| POST   | `/v1/loans/:loanId/repayments`                             | Trigger authority transfer after `repayLoan`         | 201    |
| GET    | `/v1/loans/:loanId/status`                                 | Aggregated lifecycle status enum                     | 200    |
| GET    | `/v1/deployments/:loanId`                                  | Deployment status for a specific loan                | 200    |
| GET    | `/v1/deployments?borrower=…&limit=…&offset=…`              | List deployments (paginated envelope)                | 200    |
| POST   | `/v1/jobs/expired-loan-check`                              | Kick the expired-loan recovery sweep                 | 202    |
| GET    | `/health`                                                  | Liveness probe — returns `{"status":"ok"}`           | 200    |
| GET    | `/metrics`                                                 | Prometheus exposition                                | 200    |
| GET    | `/openapi.json` / `/docs`                                  | API contract / Swagger UI                            | 200    |

### Upload Request


```
POST /v1/uploads
Content-Type: multipart/form-data

Fields:
  - file:          the .so binary
  - borrower:      wallet pubkey string
  - expectedHash:  sha256(file) in lowercase hex
                   server rejects 422 hash_mismatch if its computed hash differs
```

### Create-Loan Request

```
POST /v1/loans
Content-Type: application/json

{
  "signature": "tx_signature",
  "borrower":  "pubkey",
  "loanId":    "0",
  "fileId":    "abc123"
}
```

### Create-Repayment Request


```
POST /v1/loans/42/repayments        ← loanId is in the URL
Content-Type: application/json

{
  "signature": "tx_signature",
  "borrower":  "pubkey"
}
```

### List endpoints return a paginated envelope


```json
{
  "uploads":   [ /* FileUploadRecord, ... */ ],
  "total":     123,
  "limit":     50,
  "offset":    0,
  "hasMore":   true
}
```

Server caps `limit` at 200; default 50. The CLI's `get_uploads_by_borrower`
and `get_deployments_by_borrower` unwrap the envelope and return the
inner array for caller compatibility.

## Build & Test


```bash
cargo build --release
cargo test
cargo clippy
```

The binary outputs to `target/release/solignition`.

## Common Tasks


### Adding a new command


1. Add variant to `Commands` enum in `main.rs`
2. Add match arm calling `cmd_<name>` async function
3. Implement the function — use `client.rs` for API calls, `solana_ops.rs` for on-chain

### Adding a new on-chain instruction


1. Get the 8-byte discriminator from the IDL (`instructions[n].discriminator`)
2. Add it as a constant in `solana_ops.rs`
3. Build the accounts list matching the IDL's `accounts` array (order matters)
4. Serialize args in order: discriminator bytes + each arg in little-endian

### Verifying account layout offsets


```bash
# Fetch raw account data

solana account <PDA_ADDRESS> --output json --url <RPC_URL>

# Compare the base64-decoded bytes against expected offsets

```

## Dependencies


- `solana-sdk` / `solana-client` 2.2 — match to your cluster version
- `clap` 4 — CLI parsing with derive macros
- `reqwest` 0.12 — HTTP with multipart upload support
- `colored` / `indicatif` / `dialoguer` — terminal UX
- No Anchor runtime dependency

## Config Precedence


CLI flags > Environment variables > Config file (`~/.solignition/config.toml`) > Defaults

| Setting    | Env Var                  | CLI Flag       | Default                                          | 
|------------|--------------------------|----------------|--------------------------------------------------|
| API URL    | `SOLIGNITION_API_URL`    | `--api-url`    | `https://api.solignition.ngrok.app`                          | 
| RPC URL    | `SOLANA_RPC_URL`         | `--rpc-url`    | `https://api.devnet.solana.com`                  |
| Keypair    | `SOLIGNITION_KEYPAIR`    | `--keypair`    | `~/.config/solana/id.json`                       |
| Program ID | `SOLIGNITION_PROGRAM_ID` | `--program-id` | `HVzpjSxwECnb6uY9Jnia48oJp4xrQiz5jgc5hZC5df63`   |