Skip to content

Commit bc51008

Browse files
authored
Merge pull request #8 from oceanprotocol/feature/add-incentive-backend-api
Feature/add incentive backend api
2 parents 627b271 + 7e854cc commit bc51008

9 files changed

Lines changed: 884 additions & 12 deletions

File tree

scripts/sync-docs-content.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ const contentDir = process.env.DOCS_CONTENT_DIR
1111
: path.resolve(__dirname, '../content')
1212

1313
const repos = [
14-
{
15-
name: 'ON-Docs-MCP',
16-
envKey: 'ON_DOCS_PATH',
17-
url: 'https://github.com/oceanprotocol/ON-Docs-MCP',
18-
branch: 'main'
19-
},
2014
{
2115
name: 'ocean-node',
2216
envKey: 'OCEAN_NODE_PATH',
@@ -29,12 +23,6 @@ const repos = [
2923
url: 'https://github.com/oceanprotocol/nodes-dashboard',
3024
branch: 'main'
3125
},
32-
{
33-
name: 'nodes-incentives-monitor',
34-
envKey: 'NODES_INCENTIVES_MONITOR_PATH',
35-
url: 'https://github.com/oceanprotocol/nodes-incentives-monitor',
36-
branch: 'main'
37-
},
3826
{
3927
name: 'vscode-extension',
4028
envKey: 'VSCODE_EXTENSION_PATH',

src/clients/incentivesClient.ts

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
type JsonObject = Record<string, unknown>
2+
type SortDirection = 'asc' | 'desc'
3+
4+
type ListQueryInput = {
5+
page?: number
6+
size?: number
7+
search?: string
8+
useScroll?: boolean
9+
filters?: JsonObject
10+
sort?: Record<string, SortDirection>
11+
}
12+
13+
type NodesQueryInput = ListQueryInput & {
14+
nodeId?: string
15+
}
16+
17+
type PaginationInput = {
18+
page?: number
19+
size?: number
20+
}
21+
22+
type UnbanRequestInput = {
23+
signature: string
24+
expiryTimestamp: number
25+
address: string
26+
}
27+
28+
const DEFAULT_BASE_URL = 'https://api.oncompute.ai'
29+
const DEFAULT_TIMEOUT_MS = 20_000
30+
31+
function isJsonObject(value: unknown): value is JsonObject {
32+
return typeof value === 'object' && value !== null && !Array.isArray(value)
33+
}
34+
35+
function appendNestedQuery(
36+
params: URLSearchParams,
37+
prefix: string,
38+
value: unknown
39+
): void {
40+
if (value === undefined || value === null) return
41+
42+
if (Array.isArray(value)) {
43+
for (const item of value) {
44+
appendNestedQuery(params, `${prefix}[]`, item)
45+
}
46+
return
47+
}
48+
49+
if (isJsonObject(value)) {
50+
for (const [key, nestedValue] of Object.entries(value)) {
51+
appendNestedQuery(params, `${prefix}[${key}]`, nestedValue)
52+
}
53+
return
54+
}
55+
56+
params.append(prefix, String(value))
57+
}
58+
59+
function parseErrorDetail(responseText: string, statusText: string): string {
60+
if (!responseText.trim()) return statusText || 'Request failed'
61+
62+
try {
63+
const parsed = JSON.parse(responseText) as unknown
64+
if (isJsonObject(parsed) && typeof parsed.message === 'string') {
65+
return parsed.message
66+
}
67+
return JSON.stringify(parsed)
68+
} catch {
69+
return responseText
70+
}
71+
}
72+
73+
export class IncentivesClient {
74+
readonly baseUrl: string
75+
76+
constructor(baseUrl = process.env.INCENTIVES_API_BASE_URL ?? DEFAULT_BASE_URL) {
77+
const normalized = baseUrl.trim()
78+
if (!normalized) {
79+
throw new Error('INCENTIVES_API_BASE_URL must not be empty')
80+
}
81+
82+
this.baseUrl = normalized.endsWith('/') ? normalized : `${normalized}/`
83+
}
84+
85+
listNodes(params: NodesQueryInput = {}) {
86+
return this.request('GET', '/nodes', {
87+
query: this.buildNodesQuery(params)
88+
})
89+
}
90+
91+
runQuery(query: JsonObject) {
92+
return this.request('POST', '/query', {
93+
body: { query }
94+
})
95+
}
96+
97+
getNodeSystemStats() {
98+
return this.request('GET', '/nodeSystemStats')
99+
}
100+
101+
getNodeBenchmarkHistory(nodeId: string, params: ListQueryInput = {}) {
102+
return this.request('GET', `/nodes/${encodeURIComponent(nodeId)}/benchmarkHistory`, {
103+
query: this.buildJsonListQuery(params)
104+
})
105+
}
106+
107+
getBanStatus(nodeId: string) {
108+
return this.request('GET', `/nodes/${encodeURIComponent(nodeId)}/banStatus`)
109+
}
110+
111+
requestUnban(nodeId: string, body: UnbanRequestInput) {
112+
return this.request('POST', `/nodes/${encodeURIComponent(nodeId)}/unban`, {
113+
body
114+
})
115+
}
116+
117+
listUnbanRequests(nodeId: string, params: PaginationInput = {}) {
118+
const query = new URLSearchParams()
119+
if (params.page !== undefined) query.set('page', String(params.page))
120+
if (params.size !== undefined) query.set('size', String(params.size))
121+
122+
return this.request('GET', `/nodes/${encodeURIComponent(nodeId)}/unbanRequests`, {
123+
query
124+
})
125+
}
126+
127+
getNodeBenchmark(nodeId: string) {
128+
return this.request('GET', `/nodes/${encodeURIComponent(nodeId)}/benchmark`)
129+
}
130+
131+
listOwnerComputeJobs(owner: string, params: ListQueryInput = {}) {
132+
return this.request('GET', `/owners/${encodeURIComponent(owner)}/computeJobs`, {
133+
query: this.buildJsonListQuery(params)
134+
})
135+
}
136+
137+
getOwnerEnvInfo(owner: string) {
138+
return this.request('GET', `/owners/${encodeURIComponent(owner)}/envInfo`)
139+
}
140+
141+
getOwnerNodesStats(owner: string) {
142+
return this.request('GET', `/owners/${encodeURIComponent(owner)}/nodesStats`)
143+
}
144+
145+
getConsumerJobsSuccessRate(consumer: string) {
146+
return this.request(
147+
'GET',
148+
`/consumers/${encodeURIComponent(consumer)}/jobs-success-rate`
149+
)
150+
}
151+
152+
listAdminNodes(admin: string, params: ListQueryInput = {}) {
153+
return this.request('GET', `/admin/${encodeURIComponent(admin)}/myNodes`, {
154+
query: this.buildJsonListQuery(params)
155+
})
156+
}
157+
158+
listEnvs(params: Omit<ListQueryInput, 'search'> = {}) {
159+
return this.request('GET', '/envs', {
160+
query: this.buildJsonListQuery(params)
161+
})
162+
}
163+
164+
private buildJsonListQuery(params: Partial<ListQueryInput>): URLSearchParams {
165+
const query = new URLSearchParams()
166+
167+
if (params.page !== undefined) query.set('page', String(params.page))
168+
if (params.size !== undefined) query.set('size', String(params.size))
169+
if (params.search !== undefined) query.set('search', params.search)
170+
if (params.useScroll !== undefined) {
171+
query.set('useScroll', String(params.useScroll))
172+
}
173+
if (params.filters !== undefined) {
174+
query.set('filters', JSON.stringify(params.filters))
175+
}
176+
if (params.sort !== undefined) {
177+
query.set('sort', JSON.stringify(params.sort))
178+
}
179+
180+
return query
181+
}
182+
183+
private buildNodesQuery(params: NodesQueryInput): URLSearchParams {
184+
const query = new URLSearchParams()
185+
186+
if (params.nodeId !== undefined) query.set('nodeId', params.nodeId)
187+
if (params.page !== undefined) query.set('page', String(params.page))
188+
if (params.size !== undefined) query.set('size', String(params.size))
189+
if (params.search !== undefined) query.set('search', params.search)
190+
if (params.useScroll !== undefined) {
191+
query.set('useScroll', String(params.useScroll))
192+
}
193+
if (params.filters !== undefined) {
194+
appendNestedQuery(query, 'filters', params.filters)
195+
}
196+
if (params.sort !== undefined) {
197+
query.set('sort', JSON.stringify(params.sort))
198+
}
199+
200+
return query
201+
}
202+
203+
private async request<T = unknown>(
204+
method: 'GET' | 'POST',
205+
path: string,
206+
options: {
207+
query?: URLSearchParams
208+
body?: unknown
209+
} = {}
210+
): Promise<T> {
211+
const url = new URL(path.replace(/^\/+/, ''), this.baseUrl)
212+
if (options.query && options.query.size > 0) {
213+
url.search = options.query.toString()
214+
}
215+
216+
const init: NonNullable<Parameters<typeof fetch>[1]> = {
217+
method,
218+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
219+
}
220+
221+
if (options.body !== undefined) {
222+
init.headers = {
223+
'content-type': 'application/json'
224+
}
225+
init.body = JSON.stringify(options.body)
226+
}
227+
228+
let response: Response
229+
try {
230+
response = await fetch(url, init)
231+
} catch (error) {
232+
const message = error instanceof Error ? error.message : `${error}`
233+
throw new Error(`Incentives API ${method} ${path} request failed: ${message}`)
234+
}
235+
236+
const responseText = await response.text()
237+
if (!response.ok) {
238+
const detail = parseErrorDetail(responseText, response.statusText)
239+
throw new Error(
240+
`Incentives API ${method} ${path} failed (${response.status} ${response.statusText}): ${detail}`
241+
)
242+
}
243+
244+
if (!responseText) {
245+
return undefined as T
246+
}
247+
248+
const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''
249+
if (contentType.includes('application/json')) {
250+
return JSON.parse(responseText) as T
251+
}
252+
253+
return responseText as T
254+
}
255+
}

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { randomUUID } from 'node:crypto'
66
import type { Request, Response } from 'express'
77

88
import { ProviderInstance } from '@oceanprotocol/lib'
9+
import { IncentivesClient } from './clients/incentivesClient.js'
910
import { NodeClient } from './clients/nodeClient.js'
1011
import { loadDocs } from './docs/loader.js'
1112
import {
@@ -146,10 +147,12 @@ async function startSseServer(serverContext: ServerContext) {
146147
async function createServerContext(): Promise<ServerContext> {
147148
const evmRegistry = getEvmProviderRegistry()
148149
const nodeClient = new NodeClient()
150+
const incentivesClient = new IncentivesClient()
149151
const docsIndex = await loadDocs()
150152

151153
return {
152154
nodeClient,
155+
incentivesClient,
153156
evmRegistry,
154157
docsIndex
155158
}

src/server/serverContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { NodeClient } from '../clients/nodeClient.js'
2+
import type { IncentivesClient } from '../clients/incentivesClient.js'
23
import type { DocIndex } from '../docs/loader.js'
34
import type { EvmProviderRegistry } from '../evm/evmProviderRegistry.js'
45

56
export type ServerContext = {
67
nodeClient: NodeClient
8+
incentivesClient: IncentivesClient
79
evmRegistry: EvmProviderRegistry
810
docsIndex: DocIndex
911
}

src/test/integration/docsMerge.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ describe('docs MCP merge', () => {
102102

103103
const server = createServer({
104104
nodeClient: {} as any,
105+
incentivesClient: {} as any,
105106
evmRegistry: {
106107
getConfiguredChainIds: (): number[] => [],
107108
getProvider: (): undefined => undefined
@@ -129,6 +130,7 @@ describe('docs MCP merge', () => {
129130
registerTools({
130131
server: fake.server,
131132
nodeClient: {} as any,
133+
incentivesClient: {} as any,
132134
evmRegistry: {} as any,
133135
docsIndex
134136
})
@@ -163,6 +165,7 @@ describe('docs MCP merge', () => {
163165
registerResources({
164166
server: fake.server,
165167
nodeClient: {} as any,
168+
incentivesClient: {} as any,
166169
evmRegistry: {
167170
getConfiguredChainIds: (): number[] => [],
168171
getProvider: (): undefined => undefined

0 commit comments

Comments
 (0)