Eliascm17/swig-delegated-spend

★ 0Forks 0RustGitHub ↗Compare

README

Swig Delegated Spend

Delegated Spend is an end-to-end Solana delegated-spending system for the Swig technical challenge. It includes a Pinocchio program, shared state layouts, SDK/client helpers, a REST API, a CLI, a LaserStream/Kafka/Postgres indexer, and database models for indexed wallet/payment state.

The implementation is designed around one boundary: security-critical authority and policy enforcement live on-chain, while product labels, querying, demos, indexing, and reconciliation live off-chain.

System Design

flowchart LR
  subgraph OnChain["On-chain Solana runtime"]
    Program["Pinocchio program<br/>authority + policy enforcement"]
    Accounts["PDAs + SPL token accounts<br/>spending / receipts / escrow"]
    Program --> Accounts
  end

  subgraph Gateway["Chain access gateway"]
    Rpc["Solana RPC<br/>submit + account reads"]
    Laser["Helius LaserStream<br/>confirmed tx/account stream"]
  end

  subgraph OffChain["Off-chain app and operator environment"]
    CLI["delegated-spend CLI"]
    API["Rust API<br/>transaction builder + indexed reads"]
    SDK["Rust SDK<br/>instruction helpers + decoders"]
    Producer["indexer-producer"]
    Kafka["Redpanda/Kafka"]
    Consumer["indexer-consumer"]
    Models["models crate<br/>Diesel DB boundary"]
    Db["Postgres"]
  end

  State["state crate<br/>shared account/event ABI"]

  CLI --> API
  API --> SDK
  SDK --> State
  Program --> State
  API -->|"submit signed transactions"| Rpc
  Rpc --> Program
  Program -->|"events + account writes"| Laser
  Laser --> Producer
  Producer --> Kafka
  Kafka --> Consumer
  Consumer --> Models
  Models --> Db
  API --> Models
  API --> Db
Loading

The API runs embedded Diesel migrations when it starts. The indexer writes through models::ChainIngestBatch::upsert_atomic, so raw transactions, events, decoded account writes, and checkpoint state commit together.

Workspace Map

programs/delegated-spend/   Pinocchio on-chain program
crates/state/               fixed account layouts, policy actions, events, PDA helpers
crates/sdk/                 instruction construction and account/event decoding helpers
crates/solana-adapter/      RPC/client traits and Solana adapter layer
crates/serde-utils/         serde helpers for public API DTOs
crates/cli/                 developer CLI over the REST API
models/                     Diesel models and all database access
services/api/               REST API, auth, migrations, tx builders
services/indexer/           LaserStream producer, parser core, Kafka consumer
migrations/                 Postgres schema migrations embedded by the API
scripts/                    local setup and API-key helpers

Protocol-specific docs:

Assignment Coverage

Assignment area Implementation
Part 1: Solana program CreateSpendingAccount, FundSubaccount, delegate add/update/revoke, spend execution, escrow preauthorize/release, kill switch, replay receipts, and structured events.
Part 2: event indexer Helius LaserStream producer, Kafka handoff, restart checkpoint, idempotent Postgres upserts, decoded event/account writes.
Part 3: developer interface Rust SDK, REST API, and CLI for create/fund/delegate/policy/spend/escrow/kill/read flows.
Part 4: ledger/reconciliation Indexed payment, escrow, spending-account, event, raw transaction, and checkpoint tables with documented reconciliation path against on-chain account reads.

Required demo flows are covered by programs/delegated-spend/tests/assignment_flow_test.rs and can also be driven through the API and CLI once the local stack is running. The reviewer-oriented CLI walkthrough lives in docs/cli-demo-flows.md.

Developer Interface

The developer surface is intentionally layered:

  • The SDK owns instruction construction, PDA helpers, and account/event decoding for Rust callers.
  • The API exposes transaction-build endpoints for writes and indexed read endpoints for spending accounts, delegates, policies, payments, events, and health.
  • The CLI is a thin authenticated client over the API. It keeps signing local with --sign-and-submit, prints tables by default, and preserves exact API responses with --json.

The API list endpoints clamp limit to 1..=100; events also support an id cursor through ?cursor=<event_id>. GET /health and GET /v1/health return API status, deployed program ID, and indexer checkpoints with last processed slot/signature.

Ledger And Reconciliation

The ledger model is event-derived rather than hand-written by API routes:

  • indexed_events is append-only provenance keyed by transaction signature, instruction position, and event ordinal.
  • intent_receipt is the executed payment table: delegate, recipient token account, mint, raw amount, intent hash, slot, signature, and source event.
  • escrow_reservation tracks third-party escrow reservation and release state with reserved/released raw amounts and provenance.
  • spending_account stores the latest decoded account projection, including role/policy JSON and observed slot/signature.
  • indexer_checkpoint records the ingestion backend cursor used for restart, replay, and health reporting.

Reconciliation is currently an operator process: compare spending_account, intent_receipt, and escrow_reservation rows against fresh RPC account reads for the same PDAs, verify the decoded bytes match indexed JSON state, and replay a slot range with scripts/backfill-indexer.sh if a gap is found. A standalone reconciliation command is the next production step; the model boundary is already in place so it can report drift without bypassing Diesel-owned database access.

Local Setup

Run the preflight. On first run it creates .env from .sample.env if needed:

bash scripts/setup-repo.sh

Review local env:

$EDITOR .env

Start local services:

tilt up

Tilt starts Postgres, Redpanda, the indexer producer, the indexer consumer, and the API. The API listens on http://localhost:5001 and exposes GET /health.

Create a local API key after Postgres and the API are healthy:

scripts/create-api-key.sh --name local-cli
export DELEGATED_SPEND_API_KEY="<printed key>"

Smoke checks:

curl http://localhost:5001/health
cargo run -p delegated-spend-cli -- --help
cargo run -p delegated-spend-cli -- --api-key "$DELEGATED_SPEND_API_KEY" spending-accounts list

API docs are served by the API process:

Swagger UI:   http://localhost:5001/swagger-ui/
OpenAPI JSON: http://localhost:5001/api-docs/openapi.json

The docs routes are public for local review. API operations inside Swagger still require x-api-key authorization and the x-network-env header.

Environment

.sample.env is safe to commit and contains every expected variable. .env is private and may contain real Helius keys, API keys, and local keypair paths.

Required for local API/indexer:

DATABASE_URL
KAFKA_BROKERS
KAFKA_TOPIC
KAFKA_GROUP_ID
SWIG_DELEGATED_SPEND_PROGRAM_ID

Required for live LaserStream indexing:

HELIUS_LASERSTREAM_URL
HELIUS_LASERSTREAM_API_KEY
HELIUS_RPC_URL or DEVNET_RPC_URL
LASERSTREAM_START_SLOT (optional manual replay)
LASERSTREAM_STOP_SLOT (optional finite replay bound)

Useful for CLI demos:

DELEGATED_SPEND_API_URL
DELEGATED_SPEND_API_KEY
DELEGATED_SPEND_NETWORK
DELEGATE_KEYPAIR
DELEGATE
DEVNET_USDC_MINT

Do not commit real DELEGATED_SPEND_API_KEY, Helius keys, or absolute private keypair paths.

Program And Policy Model

flowchart TD
  Parent["Parent authority"]
  Spending["SpendingAccount PDA"]
  Manager["Root manager role"]
  Delegate["Delegate role"]
  Policy["Policy actions"]
  Vault["Vault authority PDA"]
  Receipt["IntentReceipt PDA"]
  Escrow["EscrowReservation PDA"]

  Parent --> Manager
  Manager --> Spending
  Spending --> Delegate
  Delegate --> Policy
  Spending --> Vault
  Delegate --> Receipt
  Delegate --> Escrow
Loading

The program supports SPL token delegated spend. SOL support is intentionally out of scope. Agent, card issuer, and escrow-service roles are represented as delegate authorities plus policy actions; product-specific labels live off-chain.

Policy controls include supported mint, one-shot token cap, recurring slot-window cap, destination token account cap, inclusive slot range, and a mock normalized USD oracle limit. Daily caps are modeled as slot-window recurring limits for the challenge demo.

Indexer And Database

sequenceDiagram
  participant Program
  participant Laser as LaserStream
  participant Producer
  participant Kafka
  participant Consumer
  participant DB as Postgres
  participant API

  Program-->>Laser: confirmed transaction and account updates
  Laser-->>Producer: filtered program stream
  Producer-->>Kafka: ChainIngestBatch JSON keyed by signature
  Consumer-->>DB: atomic upsert of tx, events, accounts, checkpoint
  API-->>DB: indexed reads for CLI and integrations
Loading

The database schema stores append-only event provenance plus decoded current account/payment state. See models/README.md for the ER diagram and table ownership rules.

Manual backfill uses the same producer, Kafka topic, consumer, and idempotent Postgres writes as live ingestion:

scripts/backfill-indexer.sh <start-slot> [stop-slot]

Omit stop-slot to replay from that slot and then remain live. Include it for a bounded catch-up job that exits after flushing the range.

Demo Flow Commands

Build and run program integration coverage:

cargo build-sbf --manifest-path programs/delegated-spend/Cargo.toml --arch v1
cargo test -p swig-delegated-spend --features program-tests

CLI flow shape:

cargo run -p delegated-spend-cli -- spending-accounts create --sign-and-submit
cargo run -p delegated-spend-cli -- funding create <SPENDING_ACCOUNT> --source-token-account <SOURCE_ATA> --vault-token-account <VAULT_ATA> --mint <MINT> --amount 1000000 --sign-and-submit
cargo run -p delegated-spend-cli -- delegates add <SPENDING_ACCOUNT> --delegate-authority <DELEGATE> --mint <MINT> --token-limit 1000000 --sign-and-submit
cargo run -p delegated-spend-cli -- spends execute <SPENDING_ACCOUNT> --role-id <ROLE_ID> --mint <MINT> --vault-token-account <VAULT_ATA> --recipient-token-account <RECIPIENT_ATA> --amount 1000 --intent-hash-hex <HEX_32_BYTES> --keypair "$DELEGATE_KEYPAIR" --sign-and-submit
cargo run -p delegated-spend-cli -- events list --limit 20

Use docs/cli-demo-flows.md for assignment-mapped agentic, card issuer, first-party escrow, and third-party escrow walkthroughs. Use crates/cli/README.md for the complete CLI reference and expected output.

Security Notes

  • All write instructions check required signers.
  • Program-owned accounts verify discriminator, owner, and PDA derivation.
  • Token movement uses typed Pinocchio token instructions and validates token account mint/owner facts.
  • Replay protection is account-based through deterministic receipt PDAs.
  • Kill switch blocks delegated spend and escrow release.
  • Policy decrements and token movements happen inside one Solana transaction.
  • Indexer writes are idempotent through database uniqueness and upserts.

Known limitations:

  • USD oracle support uses a mock normalized account layout rather than direct Pyth/Switchboard parsing.
  • No provider redundancy is included in the proof of concept.
  • Reconciliation is documented and model-ready, but not yet exposed as a standalone command.
  • Health reports indexer checkpoints; richer lag alerting and Prometheus metrics are production follow-ups.

Validation

Core checks:

cargo fmt --all -- --check
cargo check --workspace --all-targets
cargo test -p state
cargo test -p swig-delegated-spend --lib
cargo test -p indexer-core
cargo test -p indexer-producer
cargo test -p indexer-consumer
cargo test -p api
cargo test -p delegated-spend-cli
cargo run -p delegated-spend-cli -- --help

Program integration checks:

cargo build-sbf --manifest-path programs/delegated-spend/Cargo.toml --arch v1
cargo test -p swig-delegated-spend --features program-tests

Contributors

Eliascm17

Issues