technocore-compute 0.1.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.
package/src/types.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Types for the Technocore Compute Exchange
3
+ *
4
+ * Agents can offer compute resources and other agents can rent them,
5
+ * with payments handled via tclk HTLC/PTLC.
6
+ */
7
+
8
+ /** Compute hardware type */
9
+ export type ComputeType = 'gpu' | 'cpu' | 'tpu' | 'npu' | 'any';
10
+
11
+ /** Job status */
12
+ export type JobStatus =
13
+ | 'listed' // Provider advertised compute
14
+ | 'requested' // Consumer wants compute
15
+ | 'matched' // Provider accepted the request
16
+ | 'locked' // Payment locked via tclk
17
+ | 'running' // Compute job is executing
18
+ | 'completed' // Job finished successfully
19
+ | 'failed' // Job failed
20
+ | 'refunded'; // Payment refunded
21
+
22
+ /** Provider advertising compute resources */
23
+ export interface ComputeProvider {
24
+ /** Provider DID */
25
+ did: string;
26
+ /** Display name */
27
+ name: string;
28
+ /** Hardware available */
29
+ hardware: ComputeType;
30
+ /** GPU model if GPU (e.g. "A100", "H100", "RTX4090") */
31
+ gpuModel?: string;
32
+ /** VRAM in GB */
33
+ vramGB?: number;
34
+ /** CPU cores */
35
+ cpuCores?: number;
36
+ /** RAM in GB */
37
+ ramGB?: number;
38
+ /** Storage in GB */
39
+ storageGB?: number;
40
+ /** Price per hour in smallest FLOP unit */
41
+ pricePerHour: string;
42
+ /** Minimum job duration in seconds */
43
+ minDurationSec: number;
44
+ /** Maximum job duration in seconds */
45
+ maxDurationSec: number;
46
+ /** Available now */
47
+ available: boolean;
48
+ /** Average rating (0-5) */
49
+ rating: number;
50
+ /** Total jobs completed */
51
+ jobsCompleted: number;
52
+ /** technocore room for coordination */
53
+ room: string;
54
+ /** When this was registered */
55
+ registeredAt: number;
56
+ /** Last heartbeat */
57
+ lastHeartbeat: number;
58
+ }
59
+
60
+ /** Consumer requesting compute */
61
+ export interface ComputeRequest {
62
+ /** Request ID */
63
+ id: string;
64
+ /** Consumer DID */
65
+ consumerDid: string;
66
+ /** Hardware needed */
67
+ hardware: ComputeType;
68
+ /** Minimum GPU model */
69
+ minGpuModel?: string;
70
+ /** Minimum VRAM needed */
71
+ minVramGB?: number;
72
+ /** Minimum CPU cores */
73
+ minCpuCores?: number;
74
+ /** Minimum RAM needed */
75
+ minRamGB?: number;
76
+ /** Duration needed in seconds */
77
+ durationSec: number;
78
+ /** Max price willing to pay */
79
+ maxPricePerHour: string;
80
+ /** Job description */
81
+ description: string;
82
+ /** When requested */
83
+ createdAt: number;
84
+ /** Expiry timestamp */
85
+ expiresAt: number;
86
+ }
87
+
88
+ /** Matched job between provider and consumer */
89
+ export interface ComputeJob {
90
+ /** Job ID */
91
+ id: string;
92
+ /** Provider DID */
93
+ providerDid: string;
94
+ /** Consumer DID */
95
+ consumerDid: string;
96
+ /** Matched request */
97
+ request: ComputeRequest;
98
+ /** Provider used */
99
+ provider: ComputeProvider;
100
+ /** Current status */
101
+ status: JobStatus;
102
+ /** tclk contract ID for payment */
103
+ tclkContractId?: string;
104
+ /** Total cost */
105
+ totalCost: string;
106
+ /** Room where deal is coordinated */
107
+ room: string;
108
+ /** Created timestamp */
109
+ createdAt: number;
110
+ /** Started timestamp */
111
+ startedAt?: number;
112
+ /** Completed timestamp */
113
+ completedAt?: number;
114
+ }
115
+
116
+ /** Compute exchange stats */
117
+ export interface ExchangeStats {
118
+ totalProviders: number;
119
+ availableProviders: number;
120
+ totalJobs: number;
121
+ activeJobs: number;
122
+ completedJobs: number;
123
+ totalVolume: string;
124
+ }
@@ -0,0 +1,218 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { ComputeExchange } from '../src/exchange.js';
4
+
5
+ const SAMPLE_PROVIDER = {
6
+ did: 'did:key:z6MkTestProvider1234567890123456789012345678',
7
+ name: 'GPU Farm',
8
+ hardware: 'gpu' as const,
9
+ gpuModel: 'A100',
10
+ vramGB: 80,
11
+ pricePerHour: '5000000',
12
+ minDurationSec: 300,
13
+ maxDurationSec: 86400,
14
+ available: true,
15
+ rating: 4.8,
16
+ jobsCompleted: 42,
17
+ room: 'compute-gpu',
18
+ registeredAt: Date.now(),
19
+ lastHeartbeat: Date.now(),
20
+ };
21
+
22
+ const SAMPLE_REQUEST = {
23
+ consumerDid: 'did:key:z6MkTestConsumer12345678901234567890123456',
24
+ hardware: 'gpu' as const,
25
+ minVramGB: 24,
26
+ durationSec: 3600,
27
+ maxPricePerHour: '10000000',
28
+ description: 'Train a small model',
29
+ };
30
+
31
+ describe('ComputeExchange', () => {
32
+ describe('providers', () => {
33
+ it('registers a provider', () => {
34
+ const exchange = new ComputeExchange();
35
+ exchange.registerProvider(SAMPLE_PROVIDER);
36
+ const providers = exchange.listProviders();
37
+ assert.strictEqual(providers.length, 1);
38
+ assert.strictEqual(providers[0].did, SAMPLE_PROVIDER.did);
39
+ });
40
+
41
+ it('finds providers by hardware type', () => {
42
+ const exchange = new ComputeExchange();
43
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, hardware: 'gpu' });
44
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, did: 'did:key:z6MkCpu123', hardware: 'cpu', cpuCores: 64, ramGB: 128 });
45
+
46
+ const gpuProviders = exchange.findProviders({ hardware: 'gpu' });
47
+ assert.strictEqual(gpuProviders.length, 1);
48
+ assert.strictEqual(gpuProviders[0].hardware, 'gpu');
49
+ });
50
+
51
+ it('finds providers by VRAM', () => {
52
+ const exchange = new ComputeExchange();
53
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, vramGB: 16 });
54
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, did: 'did:key:z6MkBig123', vramGB: 80 });
55
+
56
+ const providers = exchange.findProviders({ hardware: 'gpu', minVramGB: 24 });
57
+ assert.strictEqual(providers.length, 1);
58
+ assert.strictEqual(providers[0].vramGB, 80);
59
+ });
60
+
61
+ it('finds providers by max price', () => {
62
+ const exchange = new ComputeExchange();
63
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, pricePerHour: '5000000' });
64
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, did: 'did:key:z6MkExp123', pricePerHour: '50000000' });
65
+
66
+ const providers = exchange.findProviders({ maxPricePerHour: '10000000' });
67
+ assert.strictEqual(providers.length, 1);
68
+ assert.strictEqual(providers[0].pricePerHour, '5000000');
69
+ });
70
+
71
+ it('skips unavailable providers', () => {
72
+ const exchange = new ComputeExchange();
73
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, available: false });
74
+
75
+ const providers = exchange.findProviders({ hardware: 'gpu' });
76
+ assert.strictEqual(providers.length, 0);
77
+ });
78
+
79
+ it('updates heartbeat', () => {
80
+ const exchange = new ComputeExchange();
81
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, available: false });
82
+ exchange.heartbeat(SAMPLE_PROVIDER.did);
83
+
84
+ const provider = exchange.getProvider(SAMPLE_PROVIDER.did);
85
+ assert(provider?.available);
86
+ });
87
+
88
+ it('prunes stale providers', () => {
89
+ const exchange = new ComputeExchange();
90
+ exchange.registerProvider({
91
+ ...SAMPLE_PROVIDER,
92
+ lastHeartbeat: Date.now() - 600_000, // 10 minutes ago
93
+ });
94
+ exchange.registerProvider({
95
+ ...SAMPLE_PROVIDER,
96
+ did: 'did:key:z6MkFresh1234567890123456789012345678',
97
+ lastHeartbeat: Date.now(),
98
+ });
99
+
100
+ const pruned = exchange.pruneStaleProviders(300_000);
101
+ assert.strictEqual(pruned, 1);
102
+ assert.strictEqual(exchange.listProviders().length, 1);
103
+ });
104
+ });
105
+
106
+ describe('requests', () => {
107
+ it('creates a request', () => {
108
+ const exchange = new ComputeExchange();
109
+ const request = exchange.createRequest(SAMPLE_REQUEST);
110
+ assert(request.id.startsWith('req-'));
111
+ assert.strictEqual(request.consumerDid, SAMPLE_REQUEST.consumerDid);
112
+ });
113
+
114
+ it('lists pending requests', () => {
115
+ const exchange = new ComputeExchange();
116
+ exchange.createRequest(SAMPLE_REQUEST);
117
+ const pending = exchange.listPendingRequests();
118
+ assert.strictEqual(pending.length, 1);
119
+ });
120
+
121
+ it('prunes expired requests', () => {
122
+ const exchange = new ComputeExchange();
123
+ exchange.createRequest({ ...SAMPLE_REQUEST, durationSec: -1 }); // Already expired
124
+ const pruned = exchange.pruneExpiredRequests();
125
+ assert.strictEqual(pruned, 1);
126
+ });
127
+ });
128
+
129
+ describe('jobs', () => {
130
+ it('matches a request with a provider', () => {
131
+ const exchange = new ComputeExchange();
132
+ exchange.registerProvider(SAMPLE_PROVIDER);
133
+ const request = exchange.createRequest(SAMPLE_REQUEST);
134
+ const job = exchange.matchRequest(request.id);
135
+
136
+ assert(job !== null);
137
+ assert.strictEqual(job!.providerDid, SAMPLE_PROVIDER.did);
138
+ assert.strictEqual(job!.consumerDid, SAMPLE_REQUEST.consumerDid);
139
+ assert.strictEqual(job!.status, 'matched');
140
+ });
141
+
142
+ it('returns null when no provider matches', () => {
143
+ const exchange = new ComputeExchange();
144
+ const request = exchange.createRequest({
145
+ ...SAMPLE_REQUEST,
146
+ hardware: 'tpu', // No TPU providers
147
+ });
148
+ const job = exchange.matchRequest(request.id);
149
+ assert.strictEqual(job, null);
150
+ });
151
+
152
+ it('picks cheapest provider', () => {
153
+ const exchange = new ComputeExchange();
154
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, pricePerHour: '5000000' });
155
+ exchange.registerProvider({
156
+ ...SAMPLE_PROVIDER,
157
+ did: 'did:key:z6MkCheap12345678901234567890123456',
158
+ pricePerHour: '1000000',
159
+ });
160
+ const request = exchange.createRequest(SAMPLE_REQUEST);
161
+ const job = exchange.matchRequest(request.id);
162
+
163
+ assert(job !== null);
164
+ assert.strictEqual(job!.providerDid, 'did:key:z6MkCheap12345678901234567890123456');
165
+ });
166
+
167
+ it('transitions job through states', () => {
168
+ const exchange = new ComputeExchange();
169
+ exchange.registerProvider(SAMPLE_PROVIDER);
170
+ const request = exchange.createRequest(SAMPLE_REQUEST);
171
+ const job = exchange.matchRequest(request.id)!;
172
+
173
+ assert(exchange.acceptJob(job.id));
174
+ assert.strictEqual(exchange.getJob(job.id)!.status, 'locked');
175
+
176
+ assert(exchange.startJob(job.id));
177
+ assert.strictEqual(exchange.getJob(job.id)!.status, 'running');
178
+
179
+ assert(exchange.completeJob(job.id));
180
+ assert.strictEqual(exchange.getJob(job.id)!.status, 'completed');
181
+ });
182
+
183
+ it('refunds a job', () => {
184
+ const exchange = new ComputeExchange();
185
+ exchange.registerProvider(SAMPLE_PROVIDER);
186
+ const request = exchange.createRequest(SAMPLE_REQUEST);
187
+ const job = exchange.matchRequest(request.id)!;
188
+
189
+ assert(exchange.refundJob(job.id));
190
+ assert.strictEqual(exchange.getJob(job.id)!.status, 'refunded');
191
+ });
192
+
193
+ it('updates provider stats on completion', () => {
194
+ const exchange = new ComputeExchange();
195
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, jobsCompleted: 10 });
196
+ const request = exchange.createRequest(SAMPLE_REQUEST);
197
+ const job = exchange.matchRequest(request.id)!;
198
+ exchange.acceptJob(job.id);
199
+ exchange.startJob(job.id);
200
+ exchange.completeJob(job.id);
201
+
202
+ const provider = exchange.getProvider(SAMPLE_PROVIDER.did);
203
+ assert.strictEqual(provider!.jobsCompleted, 11);
204
+ });
205
+ });
206
+
207
+ describe('stats', () => {
208
+ it('returns correct stats', () => {
209
+ const exchange = new ComputeExchange();
210
+ exchange.registerProvider(SAMPLE_PROVIDER);
211
+ exchange.registerProvider({ ...SAMPLE_PROVIDER, did: 'did:key:z6MkBusy1234567890123456789012345', available: false });
212
+
213
+ const stats = exchange.getStats();
214
+ assert.strictEqual(stats.totalProviders, 2);
215
+ assert.strictEqual(stats.availableProviders, 1);
216
+ });
217
+ });
218
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "bundler",
6
+ "declaration": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist", "test"]
18
+ }