@relaycore/sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,434 @@
1
+ import { ethers } from 'ethers';
2
+
3
+ /**
4
+ * Service Consumer SDK
5
+ *
6
+ * For agents and applications to discover services, make payments,
7
+ * and consume services on the Relay Core platform.
8
+ */
9
+
10
+ // Configuration for the SDK
11
+ export interface ConsumerSDKConfig {
12
+ apiUrl?: string;
13
+ network?: 'testnet' | 'mainnet';
14
+ }
15
+
16
+ // Service query parameters
17
+ export interface ServiceQuery {
18
+ category?: string;
19
+ minReputation?: number;
20
+ maxLatency?: number;
21
+ maxPrice?: number;
22
+ inputType?: string;
23
+ outputType?: string;
24
+ tags?: string[];
25
+ capabilities?: string[];
26
+ sortBy?: 'reputation' | 'latency' | 'price' | 'volume';
27
+ limit?: number;
28
+ }
29
+
30
+ // Discovered service
31
+ export interface DiscoveredService {
32
+ id: string;
33
+ name: string;
34
+ description: string;
35
+ category: string;
36
+ endpointUrl: string;
37
+ pricePerCall: string;
38
+ ownerAddress: string;
39
+ reputationScore: number;
40
+ successRate: number;
41
+ avgLatencyMs: number;
42
+ schema?: {
43
+ inputType?: string;
44
+ outputType?: string;
45
+ tags?: string[];
46
+ capabilities?: string[];
47
+ };
48
+ }
49
+
50
+ // Payment result
51
+ export interface PaymentResult {
52
+ paymentId: string;
53
+ txHash: string;
54
+ settled: boolean;
55
+ }
56
+
57
+ // Service call result
58
+ export interface ServiceCallResult<T = unknown> {
59
+ success: boolean;
60
+ data?: T;
61
+ error?: string;
62
+ paymentId?: string;
63
+ latencyMs: number;
64
+ }
65
+
66
+ // Workflow step
67
+ export interface WorkflowStep {
68
+ serviceId: string;
69
+ serviceName: string;
70
+ inputType: string;
71
+ outputType: string;
72
+ }
73
+
74
+ const DEFAULT_API_URL = 'https://api.relaycore.xyz';
75
+ const FACILITATOR_URL = 'https://facilitator.cronoslabs.org/v2/x402';
76
+
77
+ export class ServiceConsumerSDK {
78
+ private apiUrl: string;
79
+ private network: 'testnet' | 'mainnet';
80
+ private signer: ethers.Signer | null = null;
81
+
82
+ constructor(config: ConsumerSDKConfig = {}) {
83
+ this.apiUrl = config.apiUrl || DEFAULT_API_URL;
84
+ this.network = config.network || 'mainnet';
85
+ }
86
+
87
+ /**
88
+ * Connect a signer for payment operations
89
+ */
90
+ connectSigner(signer: ethers.Signer): void {
91
+ this.signer = signer;
92
+ }
93
+
94
+ /**
95
+ * Discover services matching criteria
96
+ */
97
+ async discoverServices(query: ServiceQuery = {}): Promise<DiscoveredService[]> {
98
+ const params = new URLSearchParams();
99
+
100
+ if (query.category) params.set('category', query.category);
101
+ if (query.minReputation) params.set('minReputation', query.minReputation.toString());
102
+ if (query.maxLatency) params.set('maxLatency', query.maxLatency.toString());
103
+ if (query.inputType) params.set('inputType', query.inputType);
104
+ if (query.outputType) params.set('outputType', query.outputType);
105
+ if (query.tags) params.set('tags', query.tags.join(','));
106
+ if (query.capabilities) params.set('capabilities', query.capabilities.join(','));
107
+ if (query.sortBy) params.set('sortBy', query.sortBy);
108
+ if (query.limit) params.set('limit', query.limit.toString());
109
+
110
+ const response = await fetch(`${this.apiUrl}/api/services?${params}`);
111
+
112
+ if (!response.ok) {
113
+ throw new Error('Failed to discover services');
114
+ }
115
+
116
+ const data = await response.json();
117
+ return (data.services || []).map(this.formatService);
118
+ }
119
+
120
+ /**
121
+ * Get service details
122
+ */
123
+ async getService(serviceId: string): Promise<DiscoveredService | null> {
124
+ const response = await fetch(`${this.apiUrl}/api/services/${serviceId}`);
125
+
126
+ if (!response.ok) {
127
+ if (response.status === 404) return null;
128
+ throw new Error('Failed to get service');
129
+ }
130
+
131
+ const data = await response.json();
132
+ return this.formatService(data);
133
+ }
134
+
135
+ /**
136
+ * Find services compatible with given input/output types
137
+ */
138
+ async findCompatibleServices(params: {
139
+ inputType?: string;
140
+ outputType?: string;
141
+ tags?: string[];
142
+ capabilities?: string[];
143
+ }): Promise<DiscoveredService[]> {
144
+ const query = new URLSearchParams();
145
+ if (params.inputType) query.set('inputType', params.inputType);
146
+ if (params.outputType) query.set('outputType', params.outputType);
147
+ if (params.tags) query.set('tags', params.tags.join(','));
148
+ if (params.capabilities) query.set('capabilities', params.capabilities.join(','));
149
+
150
+ const response = await fetch(`${this.apiUrl}/api/schemas/compatible?${query}`);
151
+
152
+ if (!response.ok) {
153
+ throw new Error('Failed to find compatible services');
154
+ }
155
+
156
+ const data = await response.json();
157
+ return (data.services || []).map(this.formatService);
158
+ }
159
+
160
+ /**
161
+ * Suggest a workflow to transform input type to output type
162
+ */
163
+ async suggestWorkflow(params: {
164
+ startInputType: string;
165
+ endOutputType: string;
166
+ maxSteps?: number;
167
+ }): Promise<WorkflowStep[][]> {
168
+ const query = new URLSearchParams({
169
+ startInputType: params.startInputType,
170
+ endOutputType: params.endOutputType,
171
+ });
172
+ if (params.maxSteps) query.set('maxSteps', params.maxSteps.toString());
173
+
174
+ const response = await fetch(`${this.apiUrl}/api/schemas/workflow?${query}`);
175
+
176
+ if (!response.ok) {
177
+ throw new Error('Failed to suggest workflow');
178
+ }
179
+
180
+ const data = await response.json();
181
+ return data.workflows || [];
182
+ }
183
+
184
+ /**
185
+ * Call a service with automatic payment handling
186
+ */
187
+ async callService<T = unknown>(params: {
188
+ serviceId: string;
189
+ endpoint?: string;
190
+ method?: 'GET' | 'POST';
191
+ body?: unknown;
192
+ headers?: Record<string, string>;
193
+ }): Promise<ServiceCallResult<T>> {
194
+ const startTime = performance.now();
195
+
196
+ if (!this.signer) {
197
+ throw new Error('Signer not connected. Call connectSigner first.');
198
+ }
199
+
200
+ // Get service details if needed
201
+ let endpoint = params.endpoint;
202
+ if (!endpoint) {
203
+ const service = await this.getService(params.serviceId);
204
+ if (!service) {
205
+ return {
206
+ success: false,
207
+ error: 'Service not found',
208
+ latencyMs: Math.round(performance.now() - startTime),
209
+ };
210
+ }
211
+ endpoint = service.endpointUrl;
212
+ }
213
+
214
+ // Initial request
215
+ let response = await fetch(endpoint, {
216
+ method: params.method || 'GET',
217
+ headers: {
218
+ 'Content-Type': 'application/json',
219
+ ...params.headers,
220
+ },
221
+ body: params.body ? JSON.stringify(params.body) : undefined,
222
+ });
223
+
224
+ let paymentId: string | undefined;
225
+
226
+ // Handle 402 Payment Required
227
+ if (response.status === 402) {
228
+ const paymentRequired = await response.json();
229
+ const requirements = paymentRequired.paymentRequirements;
230
+
231
+ if (!requirements) {
232
+ return {
233
+ success: false,
234
+ error: 'Invalid payment requirements',
235
+ latencyMs: Math.round(performance.now() - startTime),
236
+ };
237
+ }
238
+
239
+ // Make payment via facilitator
240
+ const paymentResult = await this.makePayment({
241
+ to: requirements.payTo,
242
+ amount: requirements.maxAmountRequired,
243
+ asset: requirements.asset,
244
+ });
245
+
246
+ paymentId = paymentResult.paymentId;
247
+
248
+ // Retry with payment header
249
+ response = await fetch(endpoint, {
250
+ method: params.method || 'GET',
251
+ headers: {
252
+ 'Content-Type': 'application/json',
253
+ 'X-Payment': paymentResult.txHash,
254
+ 'X-Payment-Id': paymentId,
255
+ ...params.headers,
256
+ },
257
+ body: params.body ? JSON.stringify(params.body) : undefined,
258
+ });
259
+ }
260
+
261
+ const latencyMs = Math.round(performance.now() - startTime);
262
+
263
+ if (!response.ok) {
264
+ return {
265
+ success: false,
266
+ error: `Service returned ${response.status}: ${response.statusText}`,
267
+ paymentId,
268
+ latencyMs,
269
+ };
270
+ }
271
+
272
+ const data = await response.json();
273
+ return {
274
+ success: true,
275
+ data,
276
+ paymentId,
277
+ latencyMs,
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Make a direct payment to a service provider
283
+ */
284
+ async makePayment(params: {
285
+ to: string;
286
+ amount: string;
287
+ asset?: string;
288
+ }): Promise<PaymentResult> {
289
+ if (!this.signer) {
290
+ throw new Error('Signer not connected');
291
+ }
292
+
293
+ const signerAddress = await this.signer.getAddress();
294
+
295
+ // EIP-3009 authorization for USDC transfer
296
+ const domain = {
297
+ name: 'USD Coin',
298
+ version: '2',
299
+ chainId: this.network === 'mainnet' ? 25 : 338,
300
+ verifyingContract: params.asset || '0xf951eC28187D9E5Ca673Da8FE6757E6f0Be5F77C',
301
+ };
302
+
303
+ const types = {
304
+ TransferWithAuthorization: [
305
+ { name: 'from', type: 'address' },
306
+ { name: 'to', type: 'address' },
307
+ { name: 'value', type: 'uint256' },
308
+ { name: 'validAfter', type: 'uint256' },
309
+ { name: 'validBefore', type: 'uint256' },
310
+ { name: 'nonce', type: 'bytes32' },
311
+ ],
312
+ };
313
+
314
+ const nonce = ethers.hexlify(ethers.randomBytes(32));
315
+ const validAfter = 0;
316
+ const validBefore = Math.floor(Date.now() / 1000) + 3600;
317
+
318
+ const value = {
319
+ from: signerAddress,
320
+ to: params.to,
321
+ value: ethers.parseUnits(params.amount, 6),
322
+ validAfter,
323
+ validBefore,
324
+ nonce,
325
+ };
326
+
327
+ // Sign authorization
328
+ const signature = await (this.signer as ethers.Signer & {
329
+ signTypedData: (
330
+ domain: typeof domain,
331
+ types: typeof types,
332
+ value: typeof value
333
+ ) => Promise<string>;
334
+ }).signTypedData(domain, types, value);
335
+
336
+ // Submit to facilitator
337
+ const response = await fetch(`${FACILITATOR_URL}/settle`, {
338
+ method: 'POST',
339
+ headers: {
340
+ 'Content-Type': 'application/json',
341
+ },
342
+ body: JSON.stringify({
343
+ from: signerAddress,
344
+ to: params.to,
345
+ value: params.amount,
346
+ validAfter,
347
+ validBefore,
348
+ nonce,
349
+ signature,
350
+ network: this.network === 'mainnet' ? 'cronos-mainnet' : 'cronos-testnet',
351
+ }),
352
+ });
353
+
354
+ if (!response.ok) {
355
+ throw new Error('Payment failed');
356
+ }
357
+
358
+ const result = await response.json();
359
+ return {
360
+ paymentId: `pay_${result.txHash?.slice(2, 18) || Date.now()}`,
361
+ txHash: result.txHash || '',
362
+ settled: result.success || false,
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Get service dependencies graph
368
+ */
369
+ async getServiceGraph(serviceId: string): Promise<{
370
+ dependencies: Array<{ serviceId: string; callCount: number }>;
371
+ dependents: Array<{ serviceId: string; callCount: number }>;
372
+ }> {
373
+ const [depsResponse, deptsResponse] = await Promise.all([
374
+ fetch(`${this.apiUrl}/api/services/${serviceId}/dependencies`),
375
+ fetch(`${this.apiUrl}/api/services/${serviceId}/dependents`),
376
+ ]);
377
+
378
+ const dependencies = depsResponse.ok
379
+ ? (await depsResponse.json()).dependencies
380
+ : [];
381
+ const dependents = deptsResponse.ok
382
+ ? (await deptsResponse.json()).dependents
383
+ : [];
384
+
385
+ return { dependencies, dependents };
386
+ }
387
+
388
+ /**
389
+ * Find path between two services
390
+ */
391
+ async findServicePath(
392
+ fromServiceId: string,
393
+ toServiceId: string
394
+ ): Promise<{
395
+ path: string[];
396
+ totalLatency: number;
397
+ } | null> {
398
+ const response = await fetch(
399
+ `${this.apiUrl}/api/graph/path?from=${fromServiceId}&to=${toServiceId}`
400
+ );
401
+
402
+ if (!response.ok) {
403
+ return null;
404
+ }
405
+
406
+ const data = await response.json();
407
+ return data.shortestPath || null;
408
+ }
409
+
410
+ private formatService(s: Record<string, unknown>): DiscoveredService {
411
+ return {
412
+ id: s.id as string,
413
+ name: s.name as string,
414
+ description: s.description as string,
415
+ category: s.category as string,
416
+ endpointUrl: s.endpointUrl as string,
417
+ pricePerCall: s.pricePerCall as string,
418
+ ownerAddress: s.ownerAddress as string,
419
+ reputationScore: (s.reputationScore as number) || 0,
420
+ successRate: (s.successRate as number) || 0,
421
+ avgLatencyMs: (s.avgLatencyMs as number) || 0,
422
+ schema: s.schema as DiscoveredService['schema'],
423
+ };
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Create a Service Consumer SDK instance
429
+ */
430
+ export function createConsumerSDK(config?: ConsumerSDKConfig): ServiceConsumerSDK {
431
+ return new ServiceConsumerSDK(config);
432
+ }
433
+
434
+ export default ServiceConsumerSDK;