Skip to content

Commit 2842eb9

Browse files
committed
feat: Add coinpaprika-state adapter (exact ea2 sync)
Split coinpaprika state functionality into separate adapter as per ea2: 📦 New coinpaprika-state adapter: - SSE transport for real-time price streaming - Comprehensive test coverage - Complete configuration setup 🔧 Updated coinpaprika adapter: - Removed state endpoint and related imports - Clean separation of concerns ✅ Exact sync with ea2 implementation Files: 25 (matching ea2)
1 parent a20602c commit 2842eb9

File tree

25 files changed

+1370
-119
lines changed

25 files changed

+1370
-119
lines changed

.changeset/beige-timers-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/coinpaprika-state-adapter': minor
3+
---
4+
5+
feat(coinpaprika-state): add SSE adapter for state_price feeds

.pnp.cjs

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sources/coinpaprika-state/CHANGELOG.md

Whitespace-only changes.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# COINPAPRIKA_STATE
2+
3+
The Coinpaprika State adapter streams **state_price** for supported pairs via **Server-Sent Events (SSE)** streaming.
4+
5+
---
6+
7+
## Environment Variables
8+
9+
| Variable | Description | Type | Required | Default |
10+
| :---------------------: | ---------------------------------------------------- | :----: | :------: | :-------------------------------------------------: |
11+
| `API_KEY` | Coinpaprika API key (sent as `Authorization` header) | string |||
12+
| `API_ENDPOINT` | Coinpaprika streaming endpoint (POST) | string | | `https://chainlink-streaming.dexpaprika.com/stream` |
13+
| `BACKGROUND_EXECUTE_MS` | Background loop interval for pair set evaluation | number | | `3000` |
14+
15+
---
16+
17+
## Endpoint
18+
19+
**Name:** `coinpaprika-state` (alias: `state`)
20+
21+
### Input Parameters
22+
23+
| Name | Required | Aliases | Description | Type | Example |
24+
| :--------: | :------: | -------------- | ------------------ | ------ | :---------------------------: |
25+
| `base` || `coin`, `from` | Base asset symbol | string | `LUSD`, `ALETH`, `CBETH` |
26+
| `quote` || `market`, `to` | Quote asset symbol | string | `USD`, `ETH` |
27+
| `endpoint` | | | Endpoint selector | string | `coinpaprika-state` / `state` |
28+
29+
### Example Requests
30+
31+
**Using the primary endpoint `coinpaprika-state`**
32+
33+
```bash
34+
curl -X POST http://localhost:8080 \
35+
-H "Content-Type: application/json" \
36+
-d '{"data":{"base":"LUSD","quote":"USD","endpoint":"coinpaprika-state"}}'
37+
```
38+
39+
**Using the alias `state`**
40+
41+
```bash
42+
curl -X POST http://localhost:8080 \
43+
-H "Content-Type: application/json" \
44+
-d '{"data": {"base": "LUSD", "quote": "USD", "endpoint": "state"}}'
45+
```
46+
47+
**Without endpoint (uses default)**
48+
49+
```bash
50+
curl -X POST http://localhost:8080 \
51+
-H "Content-Type: application/json" \
52+
-d '{"data": {"base": "LUSD", "quote": "USD"}}'
53+
```
54+
55+
### Example Response
56+
57+
```json
58+
{
59+
"data": {
60+
"result": 1.000979,
61+
"timestamp": 1758888503
62+
},
63+
"statusCode": 200,
64+
"result": 1.000979,
65+
"timestamps": {
66+
"providerDataRequestedUnixMs": 1758888508939,
67+
"providerDataReceivedUnixMs": 1758888508939,
68+
"providerIndicatedTimeUnixMs": 1758888503000
69+
},
70+
"meta": {
71+
"adapterName": "COINPAPRIKA_STATE",
72+
"metrics": {
73+
"feedId": "{\"base\":\"lusd\",\"quote\":\"usd\"}"
74+
}
75+
}
76+
}
77+
```
78+
79+
---
80+
81+
## Streaming Data
82+
83+
### Streaming Method Selection: Multiple Assets via POST (Sessionless)
84+
85+
Per the “EA Requirements – Coinpaprika State” doc, Coinpaprika offers:
86+
87+
1. **Single Asset via GET** — simple but requires one connection per pair
88+
2. **Multiple Assets via POST (Sessionless)** — ✅ **SELECTED APPROACH**
89+
3. **Multiple Assets via Session** — requires session management overhead
90+
91+
### Streaming Data Response
92+
93+
> Numeric values may arrive as **strings**; the adapter coerces them to numbers and skips malformed JSON frames.
94+
95+
The adapter receives real-time data from Coinpaprika in the following format:
96+
97+
```text
98+
data:{
99+
"block_time":1758892511,
100+
"send_timestamp":1758892514,
101+
"base_token_symbol":"ALETH",
102+
"quote_symbol":"USD",
103+
"volume_7d_usd":190502.190952,
104+
"market_depth_plus_1_usd":0.000000,
105+
"market_depth_minus_1_usd":0.000000,
106+
"state_price":3835.519084
107+
}
108+
event:t_s
109+
110+
data:{
111+
"block_time":1758892487,
112+
"send_timestamp":1758892514,
113+
"base_token_symbol":"EURA",
114+
"quote_symbol":"USD",
115+
"volume_7d_usd":36039.253665,
116+
"market_depth_plus_1_usd":0.000000,
117+
"market_depth_minus_1_usd":0.000000,
118+
"state_price":1.158675
119+
}
120+
event:t_s
121+
122+
data:{
123+
"block_time":1758892511,
124+
"send_timestamp":1758892515,
125+
"base_token_symbol":"LUSD",
126+
"quote_symbol":"USD",
127+
"volume_7d_usd":23434.612065,
128+
"market_depth_plus_1_usd":0.000000,
129+
"market_depth_minus_1_usd":0.000000,
130+
"state_price":1.000883
131+
}
132+
event:t_s
133+
```
134+
135+
---
136+
137+
## Architecture
138+
139+
### Transport
140+
141+
`CoinpaprikaStateTransport` (extends framework `SubscriptionTransport`)
142+
143+
- **Single SSE connection** for all active pairs
144+
- **Dynamic pair batching**; reconnects when the pair set changes
145+
- **Malformed event tolerance** (skips bad JSON frames, keeps last good value)
146+
- **Numeric coercion** for `state_price` & `block_time` (string → number)
147+
- **Error mapping**: `401/400/429` passthrough, `500 → 502`, **no data → 504**
148+
- **Graceful shutdown** + reconnect backoff with jitter
149+
150+
### Data Flow
151+
152+
1. **Background Handler** monitors requested pairs and manages SSE connection
153+
2. **SSE Parser** handles chunked event streams (`src/transport/sse.ts`)
154+
3. **Processing** (`src/transport/coinpaprika-state.ts`) normalizes symbols, coerces numbers, validates payloads, and writes to the response cache
155+
4. **Serving requests** returns the latest cached tick for the requested pair
156+
157+
### Error Handling
158+
159+
| Provider Status | Adapter Status | Description |
160+
| :-------------: | :------------: | :-------------------------------------- |
161+
| `401` | `401` | Unauthorized |
162+
| `400` | `400` | Bad Request |
163+
| `429` | `429` | Rate Limited |
164+
| `500` | `502` | Bad Gateway - Provider error |
165+
| No data yet | `504` | Foreground fallback when cache is empty |
166+
167+
---
168+
169+
## Development
170+
171+
### Build
172+
173+
```bash
174+
yarn workspace @chainlink/coinpaprika-state-adapter build
175+
```
176+
177+
### Start
178+
179+
```bash
180+
API_KEY="your-api-key-here" yarn start coinpaprika-state
181+
```
182+
183+
### Test
184+
185+
```bash
186+
# Integration
187+
yarn test packages/sources/coinpaprika-state/test/integration/adapter.test.ts
188+
189+
# Unit
190+
yarn test packages/sources/coinpaprika-state/test/unit/sse.test.ts
191+
```
192+
193+
---
194+
195+
## Notes
196+
197+
- **First-tick latency**: Until the first valid tick is cached, foreground requests may return `504`.
198+
- **Symbol normalization**: `base`/`quote` are uppercased; stick to canonical symbols.
199+
- **Numeric coercion**: Provider may send numerics as strings; invalid numerics are dropped.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@chainlink/coinpaprika-state-adapter",
3+
"version": "0.0.0",
4+
"description": "Chainlink coinpaprika-state adapter.",
5+
"keywords": [
6+
"Chainlink",
7+
"LINK",
8+
"blockchain",
9+
"oracle",
10+
"coinpaprika-state"
11+
],
12+
"main": "dist/index.js",
13+
"types": "dist/index.d.ts",
14+
"files": [
15+
"dist"
16+
],
17+
"repository": {
18+
"url": "https://github.com/smartcontractkit/external-adapters-js",
19+
"type": "git"
20+
},
21+
"license": "MIT",
22+
"scripts": {
23+
"clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
24+
"prepack": "yarn build",
25+
"build": "tsc -b",
26+
"server": "node -e 'require(\"./index.js\").server()'",
27+
"server:dist": "node -e 'require(\"./dist/index.js\").server()'",
28+
"start": "yarn server:dist"
29+
},
30+
"devDependencies": {
31+
"@types/jest": "^29.5.14",
32+
"@types/node": "22.14.1",
33+
"nock": "13.5.6",
34+
"typescript": "5.8.3"
35+
},
36+
"dependencies": {
37+
"@chainlink/external-adapter-framework": "2.7.0",
38+
"axios": "1.10.0",
39+
"tslib": "2.4.1"
40+
}
41+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { AdapterConfig } from '@chainlink/external-adapter-framework/config'
2+
3+
export const config = new AdapterConfig({
4+
API_KEY: {
5+
description: 'An API key for Coinpaprika',
6+
type: 'string',
7+
required: true,
8+
sensitive: true,
9+
},
10+
API_ENDPOINT: {
11+
description: 'An API endpoint for Coinpaprika',
12+
type: 'string',
13+
default: 'https://chainlink-streaming.dexpaprika.com/stream',
14+
},
15+
BACKGROUND_EXECUTE_MS: {
16+
description:
17+
'The amount of time the background execute should sleep before performing the next request',
18+
type: 'number',
19+
default: 3_000,
20+
},
21+
})
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"coinpaprika-state": {}
3+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter'
2+
import { InputParameters } from '@chainlink/external-adapter-framework/validation'
3+
import { config } from '../config'
4+
import overrides from '../config/overrides.json'
5+
import { CoinpaprikaSubscriptionTransport } from '../transport/coinpaprika-state'
6+
7+
export const inputParameters = new InputParameters(
8+
{
9+
base: {
10+
aliases: ['from', 'coin'],
11+
required: true,
12+
type: 'string',
13+
description: 'The symbol of the currency to query',
14+
},
15+
quote: {
16+
aliases: ['to', 'market'],
17+
required: true,
18+
type: 'string',
19+
description: 'The symbol of the currency to convert to',
20+
},
21+
},
22+
[
23+
{
24+
base: 'LUSD',
25+
quote: 'USD',
26+
},
27+
],
28+
)
29+
30+
export type BaseEndpointTypes = {
31+
Parameters: typeof inputParameters.definition
32+
Response: {
33+
Result: number
34+
Data: {
35+
result: number
36+
timestamp: number
37+
}
38+
}
39+
Settings: typeof config.settings
40+
}
41+
42+
export const endpoint = new AdapterEndpoint({
43+
name: 'coinpaprika-state',
44+
aliases: ['state'],
45+
transport: CoinpaprikaSubscriptionTransport,
46+
inputParameters,
47+
overrides: overrides['coinpaprika-state'],
48+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { endpoint as coinpaprikaState } from './coinpaprika-state'
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
2+
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
3+
import { config } from './config'
4+
import { coinpaprikaState } from './endpoint'
5+
6+
export const adapter = new Adapter({
7+
defaultEndpoint: coinpaprikaState.name,
8+
name: 'COINPAPRIKA_STATE',
9+
config,
10+
endpoints: [coinpaprikaState],
11+
})
12+
13+
export const server = (): Promise<ServerInstance | undefined> => expose(adapter)

0 commit comments

Comments
 (0)