Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Repository Guidelines

## Project Structure & Module Organization

Riskmesh is split into three workspaces. `frontend/` contains the React 19 + Vite app, with source in `frontend/src`, shared UI in `src/components`, hooks in `src/hooks`, state in `src/store`, and Vitest tests in `__tests__`. `backend/` contains the Rust Axum oracle daemon/API under `backend/src` and SQLite data under `backend/data`. `contract/` contains the Anchor program in `contract/programs/open_parametric/src`, TypeScript integration tests in `contract/tests`, and scripts in `contract/scripts`. Cross-project docs live in `docs/`.

## Build, Test, and Development Commands

- `cd frontend && npm run dev`: start the Vite dashboard locally.
- `cd frontend && npm run build`: type-check and build the frontend.
- `cd frontend && npm run lint`: run ESLint for TypeScript/React.
- `cd frontend && npm run test` or `npm run test:coverage`: run Vitest tests.
- `cd backend && cargo run --bin oracle-daemon`: start the backend daemon/API.
- `cd backend && cargo test`: run Rust backend tests.
- `cd contract && anchor build`: build the Solana program.
- `cd contract && npm run test`: run TypeScript contract tests with `ts-mocha`.
- `cd contract && npm run test:anchor`: run Anchor tests.

## Coding Style & Naming Conventions

Frontend code uses TypeScript, Emotion, ESLint, and Prettier. Follow `frontend/.prettierrc`: 2-space indentation, semicolons, single quotes, trailing commas, and 100-character print width. React components use `PascalCase`, hooks use `useCamelCase`, and tests use `*.test.ts` or `*.test.tsx`. Rust code must be `rustfmt` clean; keep instruction handlers and tests paired by feature, such as `settle_flight_claim.rs` and `settle_flight_claim_test.rs`.

## Testing Guidelines

Place frontend tests near the relevant component, hook, store, or service in `__tests__`. Prefer Vitest for pure logic and React Testing Library for rendered behavior. For contracts, use Rust unit tests for deterministic logic and Anchor/TypeScript tests for account flows. Run the narrow test first, then the workspace command before opening a PR.

## Commit & Pull Request Guidelines

Recent commits use concise imperative messages, often prefixed with `feat:` for new capabilities, for example `feat: Track B oracle via TypeScript subprocess delegation`. Keep commits scoped to one concern and call out docs, mint/program ID, or staging changes. PRs should include a summary, test results, linked issue if available, and screenshots for UI changes.

## Security & Configuration Tips

Do not commit private keys, wallet seed material, or populated `.env` files. Treat devnet program IDs, approved mint constants, and Switchboard/AviationStack settings as environment-specific; update matching frontend, backend, and contract references together.
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target/
.env
data/
riskmesh-b0fe9-firebase-adminsdk-fbsvc-46e7e0876e.json
2 changes: 0 additions & 2 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ solana-sdk = "1.18"
# Async runtime
tokio = { version = "1", features = ["full"] }

# HTTP client (AviationStack + Switchboard Crossbar)
# HTTP client (AviationStack proxy / Firebase)
reqwest = { version = "0.12", features = ["json"] }

# Scheduling
Expand All @@ -44,7 +44,5 @@ borsh = "0.10"
shellexpand = "3"
solana-account-decoder = "1.18"
sha2 = "0.10"
bincode = "1"
base64 = "0.22"
tokio-stream = { version = "0.1", features = ["sync"] }
futures-util = "0.3"
50 changes: 15 additions & 35 deletions backend/docs/backend-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ backend/
├── db.rs # SQLite 기반 PolicyRepository 구현
├── events.rs # EventBus — SSE 실시간 이벤트 브로드캐스트
├── flight_api.rs # AviationStack HTTP 클라이언트
├── switchboard.rs # Switchboard On-Demand 오라클 클라이언트
├── api/ # REST API 레이어 (Axum)
│ ├── client.rs # HTTP 클라이언트 유틸
│ ├── error.rs # API 에러 타입
Expand Down Expand Up @@ -192,50 +191,31 @@ resolve_flight_delay 인스트럭션 빌드

## Track B: Switchboard 오라클

> **대상 계정:** `Policy` (state = `Active`, leader == 내 리더 pubkey)
> **외부 API:** Switchboard Crossbar API
> **트랜잭션:** `check_oracle_and_create_claim` 인스트럭션 (v0, LUT 포함)
> **대상 계정:** `FlightPolicy` (`AwaitingOracle`, `Claimable`, `NoClaim`)
> **외부 API:** Switchboard Crossbar v2 quote
> **트랜잭션:** `check_oracle_and_resolve_flight` 이후 상태별 settle

### 파이프라인

```
[track_b::run_oracle_check]
[scheduler oracle check]
scan_active_policies()
→ get_program_accounts(POLICY discriminator)
→ borsh 디코딩으로 policy_id, leader, oracle_feed, departure_date, state 파싱
→ state == Active(3) AND leader == config.leader_pubkey 만 필터링
scan_flight_policies()
→ get_program_accounts(FlightPolicy discriminator)
→ AwaitingOracle / Claimable / NoClaim 필터링
Policy 에 대해 track_b::run() 실행
FlightPolicy 에 대해 track_b::run() 실행
시간 체크: now < departure_date → 건너뜀
AwaitingOracle: 출발 전이면 건너뜀
switchboard::fetch_oracle_update(queue, oracle_feed)
→ Crossbar API POST 호출
→ Ed25519 서명 검증 인스트럭션 + verified_update 인스트럭션 반환
→ LUT(Address Lookup Table) 계정 로드
npm run demo:5b-claim
→ Crossbar v2 feed hash로 Switchboard quote ix 생성
→ check_oracle_and_resolve_flight에서 quote 검증
oracle_round = get_slot()
Claim PDA 도출: ["claim", policy, oracle_round_le8]
check_oracle_and_create_claim 인스트럭션 빌드
→ discriminator: sha256("global:check_oracle_and_create_claim")[..8]
→ data: discriminator + oracle_round(u64 LE)
→ accounts: [policy(writable), claim(writable), payer(signer,writable),
oracle_feed, queue, slot_hashes_sysvar, instructions_sysvar,
system_program]
v0 트랜잭션 전송 (3개 인스트럭션 순서 중요!)
1. Ed25519 서명 검증 IX
2. Switchboard verified_update IX
3. check_oracle_and_create_claim IX
FlightPolicy 재조회
→ Claimable이면 settle_flight_claim
→ NoClaim이면 settle_flight_no_claim
```

### Switchboard Crossbar API (`switchboard.rs`)

- 엔드포인트: `POST https://crossbar.switchboard.xyz/updates/solana/{queue}/{feed_pubkey}`
- 응답: base64 인코딩된 인스트럭션 2개 + LUT 주소 목록 + 오라클 값(f64)
- 인스트럭션은 bincode로 역직렬화
- LUT는 온체인에서 실제 계정 데이터를 로드하여 사용

> **중요:** Track B 트랜잭션은 반드시 위 3개 인스트럭션이 **이 순서 그대로** 하나의 트랜잭션에 포함되어야 온체인 프로그램이 유효성을 검증할 수 있다.
Expand Down
10 changes: 4 additions & 6 deletions backend/docs/e2e-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,20 +219,18 @@ AVIATIONSTACK_API_KEY=<키> FLIGHT_NO=KE017 yarn demo:2-feed-create
yarn demo:3-master-setup

# FlightPolicy 생성
FLIGHT_NO=KE017 yarn demo:4-flight-create
FLIGHT_NO=KE017 npm run demo:4-flight-create
```

### Step 5. 백엔드 데몬 실행 (Track B 자동화)

> Track B 백엔드 통합은 현재 진행 중입니다.
> `check_oracle_and_resolve_flight` instruction이 온체인에 존재하며 수동 테스트는 devnet에서 진행합니다.
> 자동화 데몬 지원은 추후 추가됩니다.
> Track B 백엔드는 per-flight Switchboard quote를 검증한 뒤 상태에 따라 자동 정산합니다.

devnet 수동 확인:
```bash
cd contract
yarn demo:5b-claim # Switchboard oracle → check_oracle_and_resolve_flight
yarn demo:6-settle # 상태에 따라 settle_flight_claim 또는 settle_flight_no_claim
npm run demo:5b-claim # Switchboard quote → check_oracle_and_resolve_flight
npm run demo:6-settle # 상태에 따라 settle_flight_claim 또는 settle_flight_no_claim
```

---
Expand Down
8 changes: 6 additions & 2 deletions backend/docs/track-b-explained.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# `track_b.rs` 상세 설명

> Legacy note: 이 문서는 구 `Policy` / `check_oracle_and_create_claim` 기반 Track B를 설명합니다.
> 현재 구현은 `FlightPolicy` / `check_oracle_and_resolve_flight`와 Switchboard quote 검증 경로를 사용합니다.
> 최신 운영 흐름은 `backend/docs/backend-overview.md`의 Track B 섹션을 기준으로 보세요.

이 문서는 [track_b.rs](../src/oracle/track_b.rs)를 중심으로, Track B 오라클 파이프라인이 어떤 역할을 하고 어떤 순서로 동작하는지 설명합니다.

Track B는 Switchboard On-Demand 오라클을 사용하는 경로입니다.
Expand Down Expand Up @@ -505,7 +509,7 @@ let oracle_update =
.context("Switchboard oracle update 수신 실패")?;
```

이 부분은 [switchboard.rs](../src/switchboard.rs#L50)로 넘어가서 Crossbar HTTP API를 호출합니다.
이 부분은 당시 `backend/src/switchboard.rs` 구현으로 넘어가서 Crossbar HTTP API를 호출했습니다. 현재 Track B 구현에서는 이 파일을 제거했고, `contract/scripts/05b-claim.ts`가 per-flight feed quote를 조회합니다.

받아오는 값은 `OracleUpdate`이며 주요 내용은:

Expand Down Expand Up @@ -701,7 +705,7 @@ Track B는 이 LUT를 써야 할 수 있으므로 v0 transaction 경로를 사
## 12. 관련 파일 같이 보면 좋은 것

- [backend/src/oracle/track_b.rs](../src/oracle/track_b.rs)
- [backend/src/switchboard.rs](../src/switchboard.rs)
- `backend/src/switchboard.rs` (legacy, removed)
- [backend/src/solana/client.rs](../src/solana/client.rs)
- [backend/src/solana/pda.rs](../src/solana/pda.rs)
- [backend/src/scheduler.rs](../src/scheduler.rs)
Expand Down
3 changes: 3 additions & 0 deletions backend/src/api/service/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ fn test_config() -> Config {
db_backend: DbBackend::Sqlite,
database_path: ":memory:".to_string(),
web_bind_addr: "127.0.0.1:3000".to_string(),
contract_dir: String::new(),
proxy_url: String::new(),
}
}

Expand Down Expand Up @@ -157,6 +159,7 @@ fn flight_policy(pubkey: &str, master: &str, status: u8, child_policy_id: u64) -
premium_distributed: false,
created_at: 123,
updated_at: 124,
oracle_feed: Pubkey::new_unique().to_string(),
}
}

Expand Down
4 changes: 4 additions & 0 deletions backend/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub struct Config {
pub db_backend: DbBackend,
pub database_path: String,
pub web_bind_addr: String,
pub contract_dir: String,
pub proxy_url: String,
}

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -49,6 +51,8 @@ impl Config {
},
database_path: env("DATABASE_PATH", "data/riskmesh.db"),
web_bind_addr: env("WEB_BIND_ADDR", "0.0.0.0:3000"),
contract_dir: env("CONTRACT_DIR", "../contract"),
proxy_url: env("PROXY_URL", ""),
})
}
}
Expand Down
3 changes: 3 additions & 0 deletions backend/src/db/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ fn test_config() -> Config {
db_backend: DbBackend::Sqlite,
database_path: String::new(),
web_bind_addr: "127.0.0.1:3000".to_string(),
contract_dir: String::new(),
proxy_url: String::new(),
}
}

Expand Down Expand Up @@ -131,6 +133,7 @@ fn flight_policy(pubkey: &str, master: &str, status: u8, child_policy_id: u64) -
premium_distributed: false,
created_at: 123,
updated_at: 124,
oracle_feed: Pubkey::new_unique().to_string(),
}
}

Expand Down
1 change: 1 addition & 0 deletions backend/src/events/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ fn flight_policy(pubkey: &str, master: &str, status: u8) -> FlightPolicyInfo {
premium_distributed: false,
created_at: 123,
updated_at: 124,
oracle_feed: String::new(),
}
}

Expand Down
4 changes: 2 additions & 2 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ mod flight_api;
mod oracle;
mod scheduler;
mod solana;
mod switchboard;

use anyhow::Result;
use std::sync::Arc;
Expand All @@ -26,12 +25,13 @@ async fn main() -> Result<()> {

let config = Arc::new(config::Config::from_env()?);
tracing::info!(
"RiskMesh Backend 시작\n RPC: {}\n Program: {}\n Leader: {}\n Web: {}\n DB backend: {:?}",
"RiskMesh Backend 시작\n RPC: {}\n Program: {}\n Leader: {}\n Web: {}\n DB backend: {:?}\n Contract dir: {}",
config.rpc_url,
config.program_id,
config.leader_pubkey,
config.web_bind_addr,
config.db_backend,
config.contract_dir,
);

let repository: Arc<dyn InsuranceRepository> = match config.db_backend {
Expand Down
10 changes: 10 additions & 0 deletions backend/src/oracle/program_accounts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub struct FlightPolicyInfo {
pub premium_distributed: bool,
pub created_at: i64,
pub updated_at: i64,
pub oracle_feed: String,
}

pub fn scan_master_agreements(
Expand Down Expand Up @@ -211,6 +212,14 @@ fn parse_flight_policy(pubkey: &Pubkey, data: &[u8]) -> Result<FlightPolicyInfo>
let created_at = read_i64(data, &mut offset)?;
let updated_at = read_i64(data, &mut offset)?;
let _bump = read_u8(data, &mut offset)?;
// oracle_feed는 state.rs에서 bump 뒤에 배치됨 (기존 계정 호환을 위해 선택적 읽기)
let oracle_feed = if offset + 32 <= data.len() {
read_pubkey(data, &mut offset)
.map(|k| k.to_string())
.unwrap_or_default()
} else {
String::new()
};

Ok(FlightPolicyInfo {
pubkey: pubkey.to_string(),
Expand All @@ -230,6 +239,7 @@ fn parse_flight_policy(pubkey: &Pubkey, data: &[u8]) -> Result<FlightPolicyInfo>
premium_distributed,
created_at,
updated_at,
oracle_feed,
})
}

Expand Down
4 changes: 3 additions & 1 deletion backend/src/oracle/track_a.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::{
#[derive(Debug)]
pub struct FlightPolicyInfo {
pub pubkey: Pubkey,
pub child_policy_id: u64,
pub master_agreement: Pubkey,
pub flight_no: String,
pub departure_ts: i64,
Expand Down Expand Up @@ -81,7 +82,7 @@ pub fn parse_flight_policy(pubkey: &Pubkey, data: &[u8]) -> Result<FlightPolicyI
let mut offset = 8usize; // skip discriminator

// child_policy_id (u64)
let _child_id = read_u64(data, &mut offset)?;
let child_policy_id = read_u64(data, &mut offset)?;

// master (Pubkey, 32 bytes)
let master_agreement = read_pubkey(data, &mut offset)?;
Expand Down Expand Up @@ -118,6 +119,7 @@ pub fn parse_flight_policy(pubkey: &Pubkey, data: &[u8]) -> Result<FlightPolicyI

Ok(FlightPolicyInfo {
pubkey: *pubkey,
child_policy_id,
master_agreement,
flight_no,
departure_ts,
Expand Down
Loading
Loading