Skip to content

Commit 731c6f4

Browse files
authored
Merge pull request #103 from afurious/feat/soroban-rpc-service
feat: add soroban-rpc-service spec requirements
2 parents 9c6b44b + 4e92ba9 commit 731c6f4

2 files changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"specId": "d6ed3457-ed1e-4b20-9808-4d51ccd2c7fb", "workflowType": "requirements-first", "specType": "feature"}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Requirements Document
2+
3+
## Introduction
4+
5+
This feature introduces a dedicated, injectable `SorobanRpcService` for the niffyInsure NestJS backend. The service encapsulates all Soroban RPC / Stellar SDK interactions behind a single, well-defined boundary. It handles configurable timeouts, exponential-backoff retries for transient failures, and maps low-level Stellar SDK errors to structured NestJS HTTP exceptions so callers never need to reason about raw RPC error codes. Network selection (Futurenet / Testnet / Mainnet) is driven entirely by environment variables. The service emits Prometheus-compatible metrics and structured logs so operations teams can distinguish client faults, server faults, and provider degradation. RPC API keys are kept server-side only and never reach browser bundles.
6+
7+
> **Note on current state**: The backend currently uses Express. Migrating to NestJS (or wrapping the service in a NestJS module) is a prerequisite for the injectable pattern described here. Requirements are written against the target NestJS architecture.
8+
9+
---
10+
11+
## Glossary
12+
13+
- **SorobanRpcService**: The injectable NestJS service that is the subject of this document.
14+
- **RPC_Provider**: The remote Soroban JSON-RPC endpoint (public horizon-soroban, Quickstart, or a dedicated provider such as Ankr/Blockdaemon).
15+
- **Stellar_SDK**: The `@stellar/stellar-sdk` npm package used to construct and submit transactions.
16+
- **Simulation**: A `simulateTransaction` RPC call that dry-runs a contract invocation and returns resource footprint and result without committing to the ledger.
17+
- **Submission**: A `sendTransaction` RPC call that broadcasts a signed transaction to the network.
18+
- **Network_Config**: The tuple of `{ rpcUrl, networkPassphrase }` that identifies a Stellar network.
19+
- **Retry_Budget**: The maximum number of retry attempts allowed for a single RPC call before the service gives up and raises an error.
20+
- **Backoff_Policy**: The exponential-backoff algorithm (base delay, multiplier, jitter, max delay) applied between retries.
21+
- **Error_Classifier**: The internal component that maps raw Stellar/RPC error codes to NestJS HTTP exceptions or structured API error codes.
22+
- **Metrics_Emitter**: The component responsible for recording Prometheus counters and histograms (or equivalent structured log entries).
23+
- **Finality_Window**: The number of ledgers after which a submitted transaction is considered irreversibly confirmed or failed on Stellar (~5–7 seconds per ledger; practical finality within 1–2 ledgers).
24+
25+
---
26+
27+
## Requirements
28+
29+
### Requirement 1: NestJS Module and Injectable Service
30+
31+
**User Story:** As a backend developer, I want a NestJS module that exports `SorobanRpcService`, so that any feature module (quote, policy, claim) can inject it without managing RPC connections directly.
32+
33+
#### Acceptance Criteria
34+
35+
1. THE SorobanRpcService SHALL be decorated with `@Injectable()` and registered in a `SorobanRpcModule` that can be imported by other NestJS feature modules.
36+
2. THE SorobanRpcModule SHALL export `SorobanRpcService` so that importing modules can inject it via constructor injection.
37+
3. WHEN `SorobanRpcModule` is imported more than once in the application module graph, THE SorobanRpcModule SHALL behave as a global singleton (using `@Global()` or `isGlobal: true`) to avoid duplicate RPC connections.
38+
39+
---
40+
41+
### Requirement 2: Network Configuration via Environment Variables
42+
43+
**User Story:** As a DevOps engineer, I want to select the Stellar network and RPC endpoint through environment variables, so that the same build artifact can target Futurenet, Testnet, or Mainnet without code changes.
44+
45+
#### Acceptance Criteria
46+
47+
1. THE SorobanRpcService SHALL read `STELLAR_NETWORK` (accepted values: `futurenet`, `testnet`, `mainnet`) from the process environment at startup.
48+
2. WHEN `STELLAR_NETWORK` is `futurenet`, THE SorobanRpcService SHALL use the RPC URL from `STELLAR_RPC_URL` and the network passphrase from `STELLAR_NETWORK_PASSPHRASE`, falling back to the public Futurenet defaults if those variables are absent.
49+
3. WHEN `STELLAR_NETWORK` is `testnet`, THE SorobanRpcService SHALL use the RPC URL from `STELLAR_RPC_URL` and the network passphrase from `STELLAR_NETWORK_PASSPHRASE`, falling back to the public Testnet defaults if those variables are absent.
50+
4. WHEN `STELLAR_NETWORK` is `mainnet`, THE SorobanRpcService SHALL require both `STELLAR_RPC_URL` and `STELLAR_NETWORK_PASSPHRASE` to be explicitly set and SHALL throw a configuration error at startup if either is absent.
51+
5. IF `STELLAR_NETWORK` is set to a value other than `futurenet`, `testnet`, or `mainnet`, THEN THE SorobanRpcService SHALL throw a configuration error at application startup with a message identifying the invalid value.
52+
6. THE SorobanRpcService SHALL accept an optional `STELLAR_RPC_API_KEY` environment variable and, WHEN present, SHALL attach it as a server-side HTTP header on every RPC request.
53+
7. THE SorobanRpcService SHALL never include `STELLAR_RPC_API_KEY` in any response body, log line, or client-facing error message.
54+
55+
---
56+
57+
### Requirement 3: Configurable Timeouts
58+
59+
**User Story:** As a backend developer, I want every RPC call to have a bounded timeout, so that a slow or unresponsive provider does not block request processing indefinitely.
60+
61+
#### Acceptance Criteria
62+
63+
1. THE SorobanRpcService SHALL apply a per-call timeout to every outbound RPC request, configurable via the `STELLAR_RPC_TIMEOUT_MS` environment variable (default: 10 000 ms).
64+
2. WHEN an RPC call does not complete within the configured timeout, THE SorobanRpcService SHALL cancel the in-flight request and treat the outcome as a transient failure eligible for retry.
65+
3. THE SorobanRpcService SHALL enforce a maximum total wall-clock budget per logical operation (simulation or submission) via `STELLAR_RPC_TOTAL_TIMEOUT_MS` (default: 30 000 ms), after which no further retries SHALL be attempted regardless of remaining retry count.
66+
67+
---
68+
69+
### Requirement 4: Exponential Backoff and Retry
70+
71+
**User Story:** As a backend developer, I want transient RPC failures to be retried automatically with exponential backoff, so that brief provider hiccups do not surface as errors to callers.
72+
73+
#### Acceptance Criteria
74+
75+
1. THE SorobanRpcService SHALL retry RPC calls that fail with transient errors (network timeout, HTTP 429, HTTP 503, Soroban `tryAgainLater` status) up to a maximum of `STELLAR_RPC_MAX_RETRIES` attempts (default: 3).
76+
2. WHEN retrying, THE SorobanRpcService SHALL wait for a delay calculated as `min(baseDelay * 2^attempt + jitter, maxDelay)` where `baseDelay` defaults to 200 ms, `maxDelay` defaults to 5 000 ms, and `jitter` is a random value in `[0, baseDelay]`.
77+
3. THE SorobanRpcService SHALL NOT retry RPC calls that fail with non-transient errors (HTTP 400, invalid XDR, contract execution errors, insufficient funds).
78+
4. WHEN the Retry_Budget is exhausted without a successful response, THE SorobanRpcService SHALL stop retrying and propagate a mapped HTTP exception to the caller.
79+
5. THE SorobanRpcService SHALL log each retry attempt at `warn` level, including the attempt number, error code, and delay applied.
80+
81+
---
82+
83+
### Requirement 5: Error Mapping to NestJS HTTP Exceptions
84+
85+
**User Story:** As a backend developer, I want low-level Stellar errors mapped to standard NestJS HTTP exceptions, so that callers receive actionable, structured error responses without leaking infrastructure details.
86+
87+
#### Acceptance Criteria
88+
89+
1. WHEN an RPC call fails due to provider unavailability or exhausted retries on transient errors, THE Error_Classifier SHALL throw `HttpException` with status 502 (Bad Gateway) and a structured body containing `{ code, message }`.
90+
2. WHEN an RPC call fails because the provider is rate-limiting the service, THE Error_Classifier SHALL throw `HttpException` with status 503 (Service Unavailable) and include a `Retry-After` header when the provider supplies one.
91+
3. WHEN an RPC call fails due to a client mistake (invalid contract ID, malformed XDR, insufficient balance, bad sequence number), THE Error_Classifier SHALL throw `HttpException` with status 400 (Bad Request) and a structured body containing `{ code, message, field? }` where `code` is a stable, documented API error code.
92+
4. THE Error_Classifier SHALL map the following Soroban result codes to stable API error codes:
93+
94+
| Soroban / Stellar condition | HTTP status | Stable `code` |
95+
|---|---|---|
96+
| `tryAgainLater` (after retries) | 503 | `RPC_UNAVAILABLE` |
97+
| Provider timeout (after retries) | 502 | `RPC_TIMEOUT` |
98+
| `txBAD_SEQ` | 400 | `TX_BAD_SEQUENCE` |
99+
| `txINSUFFICIENT_BALANCE` | 400 | `TX_INSUFFICIENT_BALANCE` |
100+
| `txINSUFFICIENT_FEE` | 400 | `TX_INSUFFICIENT_FEE` |
101+
| Invalid contract ID / XDR | 400 | `TX_INVALID_INPUT` |
102+
| Contract execution failure | 400 | `CONTRACT_EXECUTION_FAILED` |
103+
| Unknown / unclassified | 502 | `RPC_UNKNOWN_ERROR` |
104+
105+
5. THE Error_Classifier SHALL never include raw RPC response bodies, stack traces, or internal hostnames in client-facing error responses.
106+
6. THE Error_Classifier SHALL log the full raw error at `error` level internally before throwing the mapped exception, so that operators can diagnose issues without exposing details to clients.
107+
108+
---
109+
110+
### Requirement 6: Simulation Helper
111+
112+
**User Story:** As a backend developer building the quote and tx-build endpoints, I want a centralized simulation helper, so that I do not duplicate `simulateTransaction` boilerplate across feature modules.
113+
114+
#### Acceptance Criteria
115+
116+
1. THE SorobanRpcService SHALL expose a `simulate(transaction: Transaction): Promise<SimulateTransactionResponse>` method that wraps `Stellar_SDK.rpc.Server.simulateTransaction`.
117+
2. WHEN simulation succeeds, THE SorobanRpcService SHALL return the raw `SimulateTransactionResponse` to the caller.
118+
3. WHEN simulation returns a `SimulateTransactionResponse` with `error` set, THE SorobanRpcService SHALL classify and throw the appropriate HTTP exception per Requirement 5.
119+
4. THE SorobanRpcService SHALL apply timeout and retry logic (Requirements 3 and 4) to simulation calls.
120+
121+
---
122+
123+
### Requirement 7: Submission Helper
124+
125+
**User Story:** As a backend developer building the tx-build endpoint, I want a centralized submission helper, so that I do not duplicate `sendTransaction` and polling boilerplate across feature modules.
126+
127+
#### Acceptance Criteria
128+
129+
1. THE SorobanRpcService SHALL expose a `submit(signedXdr: string): Promise<GetTransactionResponse>` method that sends a signed transaction and polls for its final status.
130+
2. WHEN `sendTransaction` returns status `PENDING`, THE SorobanRpcService SHALL poll `getTransaction` at configurable intervals (`STELLAR_RPC_POLL_INTERVAL_MS`, default: 2 000 ms) until the transaction reaches a terminal status (`SUCCESS`, `FAILED`, `NOT_FOUND`) or the total timeout is exceeded.
131+
3. WHEN the transaction reaches `SUCCESS`, THE SorobanRpcService SHALL return the `GetTransactionResponse` to the caller.
132+
4. WHEN the transaction reaches `FAILED` or `NOT_FOUND` after the polling window, THE SorobanRpcService SHALL throw the appropriate mapped HTTP exception per Requirement 5.
133+
5. THE SorobanRpcService SHALL apply the total wall-clock budget (Requirement 3.3) across the combined send + poll cycle.
134+
135+
---
136+
137+
### Requirement 8: Metrics and Observability
138+
139+
**User Story:** As an operations engineer, I want Prometheus-compatible metrics and structured logs from the RPC service, so that I can detect provider degradation and distinguish client, server, and provider faults.
140+
141+
#### Acceptance Criteria
142+
143+
1. THE Metrics_Emitter SHALL increment a counter `soroban_rpc_requests_total` labelled with `{ method, network, status }` on every RPC call completion, where `status` is one of `success`, `client_error`, `server_error`, `provider_error`.
144+
2. THE Metrics_Emitter SHALL record a histogram `soroban_rpc_duration_seconds` labelled with `{ method, network }` measuring the wall-clock time of each RPC call (excluding retry delays).
145+
3. THE Metrics_Emitter SHALL increment a counter `soroban_rpc_retries_total` labelled with `{ method, network }` each time a retry is attempted.
146+
4. WHEN Prometheus integration is not available, THE Metrics_Emitter SHALL emit equivalent structured JSON log entries at `info` level containing the same label/value pairs, so that log-based alerting remains possible.
147+
5. THE Metrics_Emitter SHALL label every structured log entry with `{ service: "SorobanRpcService", network, method }` to enable log-based filtering.
148+
149+
---
150+
151+
### Requirement 9: Security — API Key Isolation
152+
153+
**User Story:** As a security engineer, I want RPC API keys to remain exclusively server-side, so that they are never exposed in browser bundles, client responses, or logs.
154+
155+
#### Acceptance Criteria
156+
157+
1. THE SorobanRpcService SHALL load `STELLAR_RPC_API_KEY` only from the server-side environment and SHALL never serialize it into any HTTP response, WebSocket message, or client-accessible endpoint.
158+
2. THE SorobanRpcService SHALL redact `STELLAR_RPC_API_KEY` from all log output, replacing it with `[REDACTED]` if it would otherwise appear.
159+
3. WHERE the NestJS application uses a configuration module (e.g., `@nestjs/config`), THE SorobanRpcModule SHALL declare `STELLAR_RPC_API_KEY` as a server-only variable and SHALL NOT expose it via any public configuration endpoint.
160+
161+
---
162+
163+
### Requirement 10: Stellar Finality Documentation
164+
165+
**User Story:** As a frontend developer, I want documented finality assumptions for Stellar transactions, so that I can display accurate UX messaging about confirmation times.
166+
167+
#### Acceptance Criteria
168+
169+
1. THE SorobanRpcService SHALL include inline code documentation (JSDoc) stating that Stellar ledgers close approximately every 5–7 seconds and that practical transaction finality is achieved within 1–2 ledger closes (~10–14 seconds) under normal network conditions.
170+
2. THE SorobanRpcService SHALL document that `NOT_FOUND` status after the polling window does not guarantee the transaction was rejected — it may still be included in a future ledger — and callers SHOULD re-query before treating it as a definitive failure.
171+
3. THE SorobanRpcService SHALL document the recommended UX messaging pattern: show "confirming…" until `SUCCESS` is received, and show "check back shortly" rather than "failed" when `NOT_FOUND` is returned after the polling window.
172+
173+
---
174+
175+
### Requirement 11: Rate Limit and Provider Documentation
176+
177+
**User Story:** As a backend developer, I want documented rate limits for public RPC endpoints and guidance on when dedicated providers are required, so that I can plan capacity and avoid unexpected throttling in production.
178+
179+
#### Acceptance Criteria
180+
181+
1. THE SorobanRpcService source file SHALL include a documentation block listing the known rate limits of public Soroban RPC endpoints (Horizon-Soroban Testnet: ~100 req/min per IP; Futurenet: lower, unstable; Mainnet public: not recommended for production).
182+
2. THE SorobanRpcService documentation SHALL state that dedicated providers (Ankr, Blockdaemon, or self-hosted Quickstart) are mandatory for Mainnet production workloads exceeding 50 req/min.
183+
3. THE SorobanRpcService documentation SHALL describe the Quickstart local node setup command for integration testing so that developers can run tests without hitting public rate limits.

0 commit comments

Comments
 (0)