이슈 #314
진행중17-5. ALP 수익 계산 API
Chris Park이(가) 약 7시간 전에 추가함.
설명
GET /api/nodes/:id/earnings
CP Chris Park이(가) 7달 전에 변경 Actions #1
- 프로젝트을(를) 14에서 Alphea App(으)로 변경되었습니다.
- 목표버전을(를) 6월 중간 버전(으)로 지정되었습니다.
JL James Lee이(가) 7달 전에 변경 Actions #2
- 상태을(를) 신규에서 해결(으)로 변경되었습니다.
17-5. ALP 수익 계산 API 구현 완료¶
WalletService.GetReceipts + WalletService.GetEarningsSummary RPC 를 구현했습니다. 모두 JWT 인증이 필요한 보호 RPC 입니다.
-
Repository:
alphea-ai-edge -
Commit: commit:server|c56e3d6 —
feat: add WalletService GetReceipts and GetEarningsSummary - 관련 이슈: #254 (새 마이그레이션 0003)
구현된 RPC¶
1. GetReceipts(WalletReceiptsRequest) → WalletReceiptsResponse — 보호 RPC
message WalletReceiptsRequest {
string wallet_address = 1;
Pagination pagination = 2;
}
message ReceiptRecord {
string job_id = 1;
string node_id = 2;
JobType job_type = 3;
NodeTier tier = 4;
int32 exec_time_ms = 5;
string status = 6;
double reward_agx = 7;
double cpu_seconds = 8;
int64 network_bytes = 9;
double storage_gb = 10;
int64 completed_at = 11;
}
- 페이지네이션: 기본
limit=50, 최대 200 - 정렬:
completed_at DESC - 응답에
PageInfo { total, limit, offset, has_more }포함
2. GetEarningsSummary(WalletEarningsSummaryRequest) → WalletEarningsSummaryResponse — 보호 RPC
message WalletEarningsSummaryResponse {
double all_time_agx = 1;
double week_agx = 2;
double today_agx = 3;
}
단일 SQL 문으로 세 값을 동시에 계산 (Postgres FILTER 구문):
SELECT
COALESCE(SUM(reward_agx), 0) AS all_time,
COALESCE(SUM(reward_agx) FILTER (WHERE completed_at >= NOW() - INTERVAL '7 days'), 0) AS week,
COALESCE(SUM(reward_agx) FILTER (WHERE completed_at >= DATE_TRUNC('day', NOW())), 0) AS today
FROM job_receipts
WHERE wallet_address = $1
(wallet_address, completed_at DESC) 인덱스로 3개 파티션 모두 빠르게 스캔됩니다.
인증 모델¶
domain::auth::require_auth 를 재사용하되, WalletHandler 에 resolve_wallet 공통 헬퍼를 두어 지갑 주소 검증을 일원화했습니다:
fn resolve_wallet<T>(request, jwt_secret, requested) -> Result<String, Status> {
let caller = require_auth(request, jwt_secret)?;
if requested.is_empty() { return Ok(caller); }
if !requested.eq_ignore_ascii_case(&caller) {
return Err(Status::permission_denied(
"wallet_address does not match the authenticated caller"
));
}
Ok(requested)
}
의미:
- 요청의
wallet_address필드가 비어있으면 JWTsub를 자동 사용 - 값이 있으면 JWT
sub와 반드시 일치해야 함 (대소문자 무관) - 결과: 유효한 JWT 를 가진 사용자가 타인의 지갑 영수증을 조회할 수 없음
나머지 WalletService RPC 스텁 처리¶
현재 포팅 범위 외이므로 Status::unimplemented 로 남겼습니다:
| RPC | 용도 | 향후 계획 |
|---|---|---|
GetJobs |
지갑의 작업 이력 | 작업 이력 테이블 도입 시 구현 |
GetNodes |
지갑 보유 노드 목록 | 노드 조회는 이미 NodeService.ListNodes 로 가능. 필요성 재검토 |
ClaimVirtualNodes |
가상 노드 패키지 클레임 | 가상 노드 시스템 포팅 시 |
GetEarnings |
일별 집계 영수증 |
aggregated_earnings 테이블 도입 시 (현재는 GetReceipts 로 충분) |
GetEarningsToday |
오늘의 잔고/수익 | 가상 노드 수익 필드 때문에 가상 노드 포팅 이후 |
GetNodesDetail |
노드별 수익 상세 (주간/누적) | 별도 이슈로 분리 가능 |
GetEarningsByRange |
차트 포인트 기반 수익 집계 | 대시보드 UI 요구사항에 맞춰 |
E2E 검증 결과 (examples/wallet_earnings.rs)¶
네 개의 영수증을 시드한 뒤 각 시나리오를 검증했습니다:
시드 데이터
| # | 시점 | reward_agx | exec_ms |
|---|---|---|---|
| 0 | 방금 (today) | 0.25 | 1500 |
| 1 | 6시간 전 (today) | 0.18 | 1200 |
| 2 | 2일 전 (week) | 0.40 | 2400 |
| 3 | 10일 전 (older) | 0.17 | 900 |
결과
=== GetReceipts (owner) ===
returned=4 total=4
job=job-6dacfd65-0 ... reward=0.25 ...
job=job-6dacfd65-1 ... reward=0.18 ...
job=job-6dacfd65-2 ... reward=0.40 ...
job=job-6dacfd65-3 ... reward=0.17 ...
=== GetEarningsSummary (owner) ===
all_time=1.0000 week=0.8300 today=0.4300
expected: all_time=1.0000 week=0.8300 today=0.4300
✓ summary matches expected sums
✓ stranger blocked: PermissionDenied — wallet_address does not match the authenticated caller
✓ unauth blocked: Unauthenticated — missing authorization header
- 4건 전부 목록 반환,
PageInfo.total=4 - 합계가 클라이언트 측 예상값과 부동소수점 오차 범위 내 정확 일치
all_time = 0.25 + 0.18 + 0.40 + 0.17 = 1.0000-
week = 0.25 + 0.18 + 0.40 = 0.8300(10일 전 행 제외) today = 0.25 + 0.18 = 0.4300
- stranger JWT 로 owner 지갑 조회 시도 →
PermissionDenied - 토큰 없이 조회 →
Unauthenticated
Section 17 완료¶
| 이슈 | 제목 | 상태 |
|---|---|---|
| #310 | 17-1. 노드 목록 조회 API | ✅ 해결 |
| #311 | 17-2. 노드 상세 정보 API | ✅ 해결 |
| #312 | 17-3. 노드 추가 API | ✅ 해결 |
| #313 | 17-4. 노드 삭제 API | ✅ 해결 |
| #314 | 17-5. ALP 수익 계산 API | ✅ 해결 (이번 커밋) |
Section 17 의 모든 이슈가 포팅 완료되었습니다.
JL James Lee이(가) 7달 전에 변경 Actions #4
GetReceipts / GetEarningsSummary 를 전역 JWT 인터셉터로 전환¶
commit:server|b2c3d29
#375 에서 도입한 AuthLayer 전역 미들웨어로 Bearer JWT 검증이 이관되면서, WalletService 의 두 보호 RPC 가 더 이상 핸들러에서 JWT 를 재검증하지 않습니다.
변경 내용¶
resolve_wallet 헬퍼가 jwt_secret 을 받지 않도록 시그니처를 간소화했고, 내부적으로 caller_wallet(request) 를 호출해 미들웨어가 주입한 AuthContext 에서 wallet 을 읽습니다. wallet 불일치 검사 (PermissionDenied) 로직은 그대로.
// BEFORE
fn resolve_wallet<T>(request: &Request<T>, jwt_secret: &str, requested: &str)
-> Result<String, Status>
{
let caller = require_auth(request, jwt_secret)?;
// ...
}
// AFTER
fn resolve_wallet<T>(request: &Request<T>, requested: &str)
-> Result<String, Status>
{
let caller = caller_wallet(request)?;
// ...
}
회귀 검증¶
examples/wallet_earnings.rs 모든 단계 통과:
- 4 receipts seeded →
GetReceiptstotal=4 -
GetEarningsSummaryall_time/week/today 수치가 기대값과 정확히 일치 - stranger JWT 로 owner wallet 조회 시도 →
PermissionDenied: wallet_address does not match the authenticated caller - 토큰 없음 →
Unauthenticated: missing authorization header(미들웨어에서 반환)
관련: #375