# SDK-009 — Registration Status Polling (SDK + CLI)

**Status:** Complete
**Date:** 2026-03-17
**Branch:** `sdk-009/registration-status-polling`
**Location:** `hyperscaled-sdk/`

---

## Summary

Implemented registration status polling so SDK users and CLI operators can track a funded-account registration from `pending` through terminal success (`registered`/`active`) or terminal failure (`failed`/`eliminated`).

This work extends the `RegisterClient` added in SDK-008 with:

- a single-shot status check against `GET /api/registration-status?hl_address=...`
- a configurable polling loop that waits for a terminal status
- an `on_status` callback for callers to observe intermediate states
- a `RegistrationPollTimeoutError` with strict timeout enforcement
- optional `funded_account_id` passthrough and persistence when the backend provides it
- a full CLI `register status` command with `--poll/--no-poll`, `--json`, `--interval`, and `--timeout` options

During implementation, the status polling interface was adapted from the original sprint plan based on alignment with the actual backend contract: the backend identifies registrations by `hl_address`, not by `registration_id`.

---

## What Was Built

### Modified Files

| File | Change |
|------|--------|
| `hyperscaled/sdk/register.py` | Added `check_status_async`, `check_status`, `poll_until_complete_async`, `poll_until_complete`, strict timeout handling, and optional funded-account persistence |
| `hyperscaled/cli/register.py` | Replaced the status stub with a full `register status` command supporting single-check and polling modes, with nonzero exit on any terminal failure |
| `hyperscaled/models/registration.py` | Relaxed `status` to `str`, added `hl_address` field, made `account_size` optional, added `is_terminal` / `is_success` properties, exported `TERMINAL_STATUSES` / `SUCCESS_STATUSES` |
| `hyperscaled/models/__init__.py` | Exported `TERMINAL_STATUSES` and `SUCCESS_STATUSES` |
| `hyperscaled/exceptions.py` | Added `RegistrationPollTimeoutError` |
| `tests/test_register.py` | Added 27 tests: 11 for `check_status`, 7 for `poll_until_complete`, 9 for CLI `register status` |

### New Files

```text
hyperscaled-sdk/
└── planning/
    └── reports/
        └── SDK-009.md         # This report
```

---

## Details

### 1. Status check

`check_status_async(hl_address)` performs a single `GET /api/registration-status?hl_address=...` and returns a `RegistrationStatus`. Wallet validation is applied before the request. Backend error responses (400, 502, etc.) surface as `RegistrationError`.

If the backend includes optional fields such as `funded_account_id`, `account_size`, or `message`, they are preserved on the returned model. Successful status checks persist `funded_account_id` into local config when present.

### 2. Polling loop

`poll_until_complete_async(hl_address, *, interval_seconds, timeout_seconds, on_status)` repeatedly calls `check_status_async` until the returned status is in `TERMINAL_STATUSES` (`registered`, `active`, `failed`, `eliminated`).

- `interval_seconds` controls the delay between polls (default 5s)
- `timeout_seconds` caps the polling window and prevents sleeping past the remaining timeout budget (default 300s)
- `on_status` is invoked with each `RegistrationStatus` for progress reporting
- Raises `RegistrationPollTimeoutError` on timeout, preserving the last observed status

### 3. Terminal status classification

The `RegistrationStatus` model now includes two computed properties:

- `is_terminal` — `True` when `status` is in `{"registered", "active", "failed", "eliminated"}`
- `is_success` — `True` when `status` is in `{"registered", "active"}`

These cover both the hyperscaled app's status vocabulary (`registered`, `pending`) and the vanta-network validator's vocabulary (`active`, `eliminated`).

### 4. CLI `register status` command

```bash
# Poll until terminal (default)
hyperscaled register status --hl-wallet 0x...

# Single check, no polling
hyperscaled register status --hl-wallet 0x... --no-poll

# JSON output
hyperscaled register status --hl-wallet 0x... --no-poll --json

# Custom interval and timeout
hyperscaled register status --hl-wallet 0x... --interval 10 --timeout 600
```

Exit codes:
- `0` — terminal success (`registered` or `active`)
- `1` — terminal failure (`failed` or `eliminated`) or backend error
- `2` — polling timeout

### 5. Sprint plan adaptations

| Sprint Plan | Implementation | Reason |
|-------------|---------------|--------|
| `check_status(registration_id)` | `check_status(hl_address)` | Backend identifies registrations by HL address; `registration_id` is not returned by the app |
| Persist `funded_account_id` on success | Implemented when field is present | Current backend contract still omits `funded_account_id`, but the SDK now preserves and saves it if ENG-B-006 starts returning it |
| Expected response: `registration_id`, `funded_account_id`, `account_size` | Actual response: `status`, `hl_address`, plus optional passthrough fields when present | SDK models the current backend contract while remaining forward-compatible with richer status payloads |

---

## Acceptance Criteria Status

| Criterion | Status |
|-----------|--------|
| `client.register.check_status(hl_address)` returns structured status data | Pass |
| CLI status command displays current registration state | Pass |
| Polling stops on terminal status and surfaces a clear result | Pass |
| Unknown/missing registrations handled cleanly (`not_found` status) | Pass |
| Mock-based tests cover pending, processing, registered/active, failed, eliminated, not_found, and timeout | Pass |
| Successful completion persists `funded_account_id` to local config | Partial — implemented when the backend returns `funded_account_id`; current live contract still omits it |

---

## Verification

```bash
source .venv/bin/activate
pytest -q tests/test_register.py  → 46 passed in 0.46s
ruff check .                      → All checks passed!
ruff format --check .             → 45 files already formatted
```

---

## Test Coverage

| Test Area | Coverage |
|-----------|----------|
| Status: active | `check_status_async` returns terminal success |
| Status: pending | `check_status_async` returns non-terminal |
| Status: processing | `check_status_async` returns non-terminal |
| Status: not_found | `check_status_async` returns non-terminal |
| Status: failed | `check_status_async` returns terminal failure |
| Status: eliminated | `check_status_async` returns terminal failure |
| Status payload passthrough | Optional fields like `funded_account_id` and `account_size` are preserved |
| Config persistence | Successful status checks save `funded_account_id` when present |
| Wallet validation | Invalid address rejected before request |
| Backend 400 | Error response surfaces as `RegistrationError` |
| Backend 502 | Error response surfaces as `RegistrationError` |
| Sync usage | `check_status()` works outside event loop |
| Poll: immediate terminal | Single poll returns active |
| Poll: pending → active | Multiple polls transition to terminal |
| Poll: not_found → registered | Multi-step transition |
| Poll: reaches failed | Failure is a valid terminal state |
| Poll: timeout | `RegistrationPollTimeoutError` raised with last status |
| Poll: timeout boundary | Sleep is capped to the remaining timeout budget |
| Poll: sync | `poll_until_complete()` works outside event loop |
| CLI: no-poll active | Single check renders panel |
| CLI: no-poll JSON | JSON output of status |
| CLI: poll completes | Polling flow renders completion panel |
| CLI: poll timeout | Exit code 2 on timeout |
| CLI: failed registration | Exit code 1 on terminal failure |
| CLI: eliminated registration | Exit code 1 on terminal failure |
| CLI: invalid wallet | Rejected before request |
| CLI: backend error | Error surfaced to user |
| CLI: configured wallet | Falls back to config when `--hl-wallet` not passed |

---

## Open Follow-up Items

- Validate `funded_account_id` persistence end-to-end once ENG-B-006 returns it in live responses
- The `/api/registration-status` route currently depends on `/hl-traders/<hl_address>` on the validator, which is not yet implemented in vanta-network — end-to-end testing will require that endpoint
- Consider adding a `--wait` flag to `hyperscaled register purchase` that chains into `poll_until_complete` after a successful purchase
- Verify real polling behavior once ENG-B-006 is deployed
