stellarium-ts 0.19.0 → 0.20.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/dist/client/operatorClient.d.ts +34 -3
- package/dist/client/operatorClient.d.ts.map +1 -1
- package/dist/client/operatorClient.js +15 -0
- package/dist/contract/repository.d.ts +49 -0
- package/dist/contract/repository.d.ts.map +1 -0
- package/dist/contract/repository.js +80 -0
- package/dist/contract/service.d.ts +103 -0
- package/dist/contract/service.d.ts.map +1 -0
- package/dist/contract/service.js +297 -0
- package/dist/contract/state.d.ts +43 -0
- package/dist/contract/state.d.ts.map +1 -0
- package/dist/contract/state.js +79 -0
- package/dist/db/database.d.ts +25 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +1 -0
- package/dist/db/db.d.ts +12 -4
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +97 -24
- package/dist/db/postgres.d.ts +39 -0
- package/dist/db/postgres.d.ts.map +1 -0
- package/dist/db/postgres.js +132 -0
- package/dist/db/sqlite.d.ts +16 -0
- package/dist/db/sqlite.d.ts.map +1 -0
- package/dist/db/sqlite.js +53 -0
- package/dist/grant/repository.d.ts +2 -2
- package/dist/grant/repository.d.ts.map +1 -1
- package/dist/grant/repository.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/peer/repository.d.ts +2 -2
- package/dist/peer/repository.d.ts.map +1 -1
- package/dist/peer/repository.js +1 -1
- package/dist/resource/invoker.d.ts +85 -0
- package/dist/resource/invoker.d.ts.map +1 -1
- package/dist/resource/invoker.js +69 -6
- package/dist/resource/repository.d.ts +51 -10
- package/dist/resource/repository.d.ts.map +1 -1
- package/dist/resource/repository.js +116 -61
- package/dist/resource/settlement.d.ts +5 -0
- package/dist/resource/settlement.d.ts.map +1 -1
- package/dist/resource/settlement.js +19 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +161 -25
- package/dist/server.d.ts +30 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +80 -5
- package/dist/service.d.ts +27 -2
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +16 -1
- package/dist/startup.d.ts +2 -2
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +43 -34
- package/dist/tests/contract/repository.test.d.ts +2 -0
- package/dist/tests/contract/repository.test.d.ts.map +1 -0
- package/dist/tests/contract/repository.test.js +68 -0
- package/dist/tests/contract/routes.test.d.ts +2 -0
- package/dist/tests/contract/routes.test.d.ts.map +1 -0
- package/dist/tests/contract/routes.test.js +252 -0
- package/dist/tests/contract/state-routes.test.d.ts +2 -0
- package/dist/tests/contract/state-routes.test.d.ts.map +1 -0
- package/dist/tests/contract/state-routes.test.js +158 -0
- package/dist/tests/contract/state.test.d.ts +2 -0
- package/dist/tests/contract/state.test.d.ts.map +1 -0
- package/dist/tests/contract/state.test.js +57 -0
- package/dist/tests/db/db.test.js +1 -1
- package/dist/tests/db/postgres.test.d.ts +2 -0
- package/dist/tests/db/postgres.test.d.ts.map +1 -0
- package/dist/tests/db/postgres.test.js +43 -0
- package/dist/tests/grant/service.test.js +3 -4
- package/dist/tests/helpers.d.ts +8 -0
- package/dist/tests/helpers.d.ts.map +1 -1
- package/dist/tests/helpers.js +37 -0
- package/dist/tests/integration/network.test.js +74 -4
- package/dist/tests/peer/repository.test.js +3 -4
- package/dist/tests/resource/repository.test.js +3 -4
- package/dist/tests/routes.test.js +10 -11
- package/dist/tests/server.test.js +30 -0
- package/dist/tests/startup.test.js +4 -5
- package/package.json +5 -2
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import { isFreshExpiry, safeVerifyData } from "../crypto/utils.js";
|
|
2
2
|
import { isInferenceManifest } from "./inference.js";
|
|
3
|
+
/** The one function a contract's resource has. It runs the contract's latest version. */
|
|
4
|
+
export const CONTRACT_FUNCTION = 'run';
|
|
5
|
+
export function isContractManifest(manifest) {
|
|
6
|
+
return typeof manifest === 'object' && manifest !== null && 'priceMicroUsdPerMillionTokens' in manifest;
|
|
7
|
+
}
|
|
8
|
+
export function isContractFunction(fn) {
|
|
9
|
+
return fn.kind === 'contract';
|
|
10
|
+
}
|
|
11
|
+
/** The resource a contract is served as. Its function always has a manifest, so every call needs a voucher. */
|
|
12
|
+
export function contractResource(contract, baseUrl, priceMicroUsdPerMillionTokens, recipientOrganizationId) {
|
|
13
|
+
return {
|
|
14
|
+
id: 0,
|
|
15
|
+
baseUrl,
|
|
16
|
+
metadata: {},
|
|
17
|
+
path: contract.path,
|
|
18
|
+
name: contract.name,
|
|
19
|
+
description: contract.description ?? undefined,
|
|
20
|
+
functions: [{
|
|
21
|
+
name: CONTRACT_FUNCTION,
|
|
22
|
+
kind: 'contract',
|
|
23
|
+
parameters: contract.parameters,
|
|
24
|
+
returnType: contract.returnType,
|
|
25
|
+
exec: async () => {
|
|
26
|
+
throw new Error('Contracts are run by the resource invoker');
|
|
27
|
+
},
|
|
28
|
+
paymentManifest: { priceMicroUsdPerMillionTokens, recipientOrganizationId },
|
|
29
|
+
}],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
3
32
|
/**
|
|
4
33
|
* The object a node signs to broadcast its resources. Key order is part of the signed bytes.
|
|
5
34
|
* Functions' `exec` implementations are dropped by JSON serialization on both ends.
|
|
@@ -25,49 +54,53 @@ export class ResourceRepository {
|
|
|
25
54
|
resources;
|
|
26
55
|
baseUrl;
|
|
27
56
|
organizationId;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
contracts;
|
|
58
|
+
/**
|
|
59
|
+
* `organizationId` is the operator organization this node is enrolled in, which is paid for its functions.
|
|
60
|
+
* The contracts deployed to the node are its resources too, after the registered ones.
|
|
61
|
+
*/
|
|
62
|
+
constructor(db, resources = [], baseUrl = '', organizationId, contracts) {
|
|
32
63
|
this.db = db;
|
|
33
64
|
this.resources = resources;
|
|
34
65
|
this.baseUrl = baseUrl;
|
|
35
66
|
this.organizationId = organizationId;
|
|
67
|
+
this.contracts = contracts;
|
|
36
68
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
return run;
|
|
69
|
+
/** A resource registered on this node. */
|
|
70
|
+
isRegistered(resource) {
|
|
71
|
+
return this.resources.some(r => r.path === resource.path && r.name === resource.name);
|
|
41
72
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const result = await work();
|
|
47
|
-
await this.db.exec('COMMIT');
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
await this.db.exec('ROLLBACK');
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
});
|
|
73
|
+
/** A resource this node hosts: registered, or a contract deployed to it. */
|
|
74
|
+
async isInternal(resource) {
|
|
75
|
+
return this.isRegistered(resource) || await this.contracts?.repository.owner(resource.path, resource.name) !== undefined;
|
|
55
76
|
}
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
/** A resource learned from a peer. */
|
|
78
|
+
async isLearned(path, name) {
|
|
79
|
+
return await this.db.get('SELECT id FROM resource WHERE path = ? AND name = ?', [path, name]) !== undefined;
|
|
80
|
+
}
|
|
81
|
+
async contractResources() {
|
|
82
|
+
if (!this.contracts) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const { repository, price, organizationId } = this.contracts;
|
|
86
|
+
return (await repository.allLatest()).map(contract => contractResource(contract, this.baseUrl, price(), organizationId()));
|
|
87
|
+
}
|
|
88
|
+
async contractResource(path, name) {
|
|
89
|
+
const contract = await this.contracts?.repository.latest(path, name);
|
|
90
|
+
return contract && contractResource(contract, this.baseUrl, this.contracts.price(), this.contracts.organizationId());
|
|
58
91
|
}
|
|
59
92
|
/**
|
|
60
93
|
* Saves a resource learned from the starting peer, if it isn't known yet. Resources hosted
|
|
61
94
|
* by this node and resources already known are left alone, so startup can run on every boot.
|
|
62
95
|
*/
|
|
63
96
|
async learn(resource) {
|
|
64
|
-
if (this.isInternal(resource) || resource.baseUrl === this.baseUrl) {
|
|
97
|
+
if (await this.isInternal(resource) || resource.baseUrl === this.baseUrl) {
|
|
65
98
|
return;
|
|
66
99
|
}
|
|
67
|
-
await this.transaction(async () => {
|
|
68
|
-
const existing = await
|
|
100
|
+
await this.db.transaction(async (tx) => {
|
|
101
|
+
const existing = await tx.get('SELECT id FROM resource WHERE path = ? AND name = ?', [resource.path, resource.name]);
|
|
69
102
|
if (!existing) {
|
|
70
|
-
await this.insert(resource, null);
|
|
103
|
+
await this.insert(tx, resource, null);
|
|
71
104
|
}
|
|
72
105
|
});
|
|
73
106
|
}
|
|
@@ -83,15 +116,17 @@ export class ResourceRepository {
|
|
|
83
116
|
return { ok: false };
|
|
84
117
|
}
|
|
85
118
|
const rejected = [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
119
|
+
// Checked before the transaction, whose work may use only its own connection.
|
|
120
|
+
const internal = await Promise.all(resources.map(resource => this.isInternal(resource)));
|
|
121
|
+
await this.db.transaction(async (tx) => {
|
|
122
|
+
for (const [index, resource] of resources.entries()) {
|
|
123
|
+
if (resource.baseUrl !== peer.baseUrl || internal[index]) {
|
|
89
124
|
rejected.push(resourceKey(resource));
|
|
90
125
|
continue;
|
|
91
126
|
}
|
|
92
|
-
const existing = await
|
|
127
|
+
const existing = await tx.get('SELECT * FROM resource WHERE path = ? AND name = ?', [resource.path, resource.name]);
|
|
93
128
|
if (!existing) {
|
|
94
|
-
await this.insert(resource, peer.id);
|
|
129
|
+
await this.insert(tx, resource, peer.id);
|
|
95
130
|
continue;
|
|
96
131
|
}
|
|
97
132
|
// Unowned rows came from the starting peer; the peer they point at may claim them.
|
|
@@ -100,37 +135,39 @@ export class ResourceRepository {
|
|
|
100
135
|
rejected.push(resourceKey(resource));
|
|
101
136
|
continue;
|
|
102
137
|
}
|
|
103
|
-
await this.deleteFunctions(existing.id);
|
|
104
|
-
await
|
|
105
|
-
await this.insertFunctions(existing.id, resource.functions);
|
|
138
|
+
await this.deleteFunctions(tx, existing.id);
|
|
139
|
+
await tx.run('UPDATE resource SET base_url = ?, website = ?, description = ?, peer_id = ?, updated_at = ? WHERE id = ?', [resource.baseUrl, optionalWebsite(resource.website), optionalText(resource.description), peer.id, new Date().toISOString(), existing.id]);
|
|
140
|
+
await this.insertFunctions(tx, existing.id, resource.functions);
|
|
106
141
|
}
|
|
107
142
|
});
|
|
108
143
|
return { ok: true, rejected };
|
|
109
144
|
}
|
|
110
|
-
async insert(resource, peerId) {
|
|
145
|
+
async insert(tx, resource, peerId) {
|
|
111
146
|
const createdAt = new Date().toISOString();
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
147
|
+
const row = await tx.get('INSERT INTO resource (path, name, base_url, website, description, peer_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id', [resource.path, resource.name, resource.baseUrl, optionalWebsite(resource.website), optionalText(resource.description), peerId, createdAt, createdAt]);
|
|
148
|
+
if (!row) {
|
|
114
149
|
throw new Error('Failed to create resource');
|
|
115
150
|
}
|
|
116
|
-
await this.insertFunctions(
|
|
151
|
+
await this.insertFunctions(tx, row.id, resource.functions);
|
|
117
152
|
}
|
|
118
|
-
async insertFunctions(resourceId, functions) {
|
|
153
|
+
async insertFunctions(tx, resourceId, functions) {
|
|
119
154
|
const createdAt = new Date().toISOString();
|
|
120
155
|
for (const fn of functions) {
|
|
121
|
-
const
|
|
122
|
-
const functionId =
|
|
156
|
+
const functionRow = await tx.get('INSERT INTO resource_function (resource_id, name, return_type, description, restricted, kind, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id', [resourceId, fn.name, fn.returnType, optionalText(fn.description), fn.restricted === true ? 1 : 0, fn.kind === 'inference' || fn.kind === 'contract' ? fn.kind : 'fixed', createdAt, createdAt]);
|
|
157
|
+
const functionId = functionRow.id;
|
|
123
158
|
for (const parameter of fn.parameters) {
|
|
124
|
-
await
|
|
159
|
+
await tx.run('INSERT INTO resource_function_parameter (resource_function_id, name, parameter_type, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)', [functionId, parameter.name, parameter.type, optionalText(parameter.description), createdAt, createdAt]);
|
|
125
160
|
}
|
|
126
161
|
const manifest = fn.paymentManifest;
|
|
127
162
|
if (manifest) {
|
|
128
|
-
const inference = isInferenceManifest(manifest);
|
|
129
|
-
|
|
163
|
+
const inference = isInferenceManifest(manifest) ? manifest : undefined;
|
|
164
|
+
const contract = isContractManifest(manifest) ? manifest : undefined;
|
|
165
|
+
await tx.run('INSERT INTO resource_function_payment_manifest (resource_function_id, cost_micro_usd, input_cost_micro_usd_per_million_tokens, output_cost_micro_usd_per_million_tokens, price_micro_usd_per_million_tokens, recipient_organization_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [
|
|
130
166
|
functionId,
|
|
131
|
-
inference ? null : manifest.costMicroUsd,
|
|
132
|
-
inference ?
|
|
133
|
-
inference ?
|
|
167
|
+
inference || contract ? null : manifest.costMicroUsd,
|
|
168
|
+
inference ? inference.inputCostMicroUsdPerMillionTokens : null,
|
|
169
|
+
inference ? inference.outputCostMicroUsdPerMillionTokens : null,
|
|
170
|
+
contract ? contract.priceMicroUsdPerMillionTokens : null,
|
|
134
171
|
manifest.recipientOrganizationId ?? null,
|
|
135
172
|
createdAt,
|
|
136
173
|
createdAt,
|
|
@@ -138,14 +175,14 @@ export class ResourceRepository {
|
|
|
138
175
|
}
|
|
139
176
|
}
|
|
140
177
|
}
|
|
141
|
-
async deleteFunctions(resourceId) {
|
|
178
|
+
async deleteFunctions(tx, resourceId) {
|
|
142
179
|
const functionIds = 'SELECT id FROM resource_function WHERE resource_id = ?';
|
|
143
|
-
await
|
|
144
|
-
await
|
|
145
|
-
await
|
|
180
|
+
await tx.run(`DELETE FROM resource_function_payment_manifest WHERE resource_function_id IN (${functionIds})`, [resourceId]);
|
|
181
|
+
await tx.run(`DELETE FROM resource_function_parameter WHERE resource_function_id IN (${functionIds})`, [resourceId]);
|
|
182
|
+
await tx.run('DELETE FROM resource_function WHERE resource_id = ?', [resourceId]);
|
|
146
183
|
}
|
|
147
184
|
async getInternalResource(path, name) {
|
|
148
|
-
return this.resources.find(r => r.path === path && r.name === name);
|
|
185
|
+
return this.resources.find(r => r.path === path && r.name === name) ?? await this.contractResource(path, name);
|
|
149
186
|
}
|
|
150
187
|
async getResource(key) {
|
|
151
188
|
const resource = this.resources.find(r => resourceKey(r) === key);
|
|
@@ -156,6 +193,10 @@ export class ResourceRepository {
|
|
|
156
193
|
const pathName = key.split('.');
|
|
157
194
|
const name = pathName.pop();
|
|
158
195
|
const path = pathName.join('.');
|
|
196
|
+
const contract = await this.contractResource(path, name);
|
|
197
|
+
if (contract) {
|
|
198
|
+
return contract;
|
|
199
|
+
}
|
|
159
200
|
const result = await this.db.get('SELECT * FROM resource WHERE path = ? AND name = ?', [path, name]);
|
|
160
201
|
if (!result) {
|
|
161
202
|
return undefined;
|
|
@@ -170,19 +211,25 @@ export class ResourceRepository {
|
|
|
170
211
|
}
|
|
171
212
|
};
|
|
172
213
|
}
|
|
173
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* This node's resources, with its base URL and its organization as the recipient of every paid function:
|
|
216
|
+
* the registered ones, then its contracts by path and name.
|
|
217
|
+
*/
|
|
174
218
|
async getInternalResources() {
|
|
175
|
-
|
|
219
|
+
const registered = this.resources.map(r => ({
|
|
176
220
|
...r,
|
|
177
221
|
baseUrl: this.baseUrl,
|
|
178
222
|
functions: r.functions.map((fn) => fn.paymentManifest
|
|
179
223
|
? { ...fn, paymentManifest: { ...fn.paymentManifest, recipientOrganizationId: this.organizationId } }
|
|
180
224
|
: fn),
|
|
181
225
|
}));
|
|
226
|
+
return [...registered, ...await this.contractResources()];
|
|
182
227
|
}
|
|
183
228
|
/** Learned resources in insertion order. A negative `limit` means no limit. */
|
|
184
229
|
async getExternalResources(offset = 0, limit = -1) {
|
|
185
|
-
const results = await this.db.all('SELECT * FROM resource ORDER BY id LIMIT ? OFFSET ?',
|
|
230
|
+
const results = await this.db.all('SELECT * FROM resource ORDER BY id LIMIT ? OFFSET ?',
|
|
231
|
+
// Postgres refuses a negative limit, and SQLite needs one before an offset.
|
|
232
|
+
[limit < 0 ? Number.MAX_SAFE_INTEGER : limit, offset]);
|
|
186
233
|
return Promise.all(results.map(async (r) => ({
|
|
187
234
|
id: r.id,
|
|
188
235
|
path: r.path,
|
|
@@ -212,11 +259,11 @@ export class ResourceRepository {
|
|
|
212
259
|
exec: async () => undefined,
|
|
213
260
|
paymentManifest: await this.getResourceFunctionPaymentManifest(fn.id),
|
|
214
261
|
restricted: fn.restricted === 1 || undefined,
|
|
215
|
-
kind: fn.kind === 'inference'
|
|
262
|
+
kind: fn.kind === 'inference' || fn.kind === 'contract' ? fn.kind : undefined,
|
|
216
263
|
};
|
|
217
264
|
}
|
|
218
265
|
async getResourceFunctions(resourceId) {
|
|
219
|
-
const rows = await this.db.all('SELECT * FROM resource_function WHERE resource_id = ?', [resourceId]);
|
|
266
|
+
const rows = await this.db.all('SELECT * FROM resource_function WHERE resource_id = ? ORDER BY name', [resourceId]);
|
|
220
267
|
return Promise.all(rows.map(async (f) => ({
|
|
221
268
|
id: f.id,
|
|
222
269
|
name: f.name,
|
|
@@ -226,7 +273,7 @@ export class ResourceRepository {
|
|
|
226
273
|
exec: async () => undefined,
|
|
227
274
|
paymentManifest: await this.getResourceFunctionPaymentManifest(f.id),
|
|
228
275
|
restricted: f.restricted === 1 || undefined,
|
|
229
|
-
kind: f.kind === 'inference'
|
|
276
|
+
kind: f.kind === 'inference' || f.kind === 'contract' ? f.kind : undefined,
|
|
230
277
|
})));
|
|
231
278
|
}
|
|
232
279
|
async getResourceParameters(resourceFunctionId) {
|
|
@@ -241,6 +288,13 @@ export class ResourceRepository {
|
|
|
241
288
|
if (!result) {
|
|
242
289
|
return undefined;
|
|
243
290
|
}
|
|
291
|
+
if (result.price_micro_usd_per_million_tokens !== null) {
|
|
292
|
+
return {
|
|
293
|
+
id: result.id,
|
|
294
|
+
priceMicroUsdPerMillionTokens: result.price_micro_usd_per_million_tokens,
|
|
295
|
+
recipientOrganizationId: result.recipient_organization_id ?? undefined,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
244
298
|
if (result.cost_micro_usd === null) {
|
|
245
299
|
return {
|
|
246
300
|
id: result.id,
|
|
@@ -260,12 +314,13 @@ export class ResourceRepository {
|
|
|
260
314
|
* Both are in a fixed order, so offsets stay stable as resources are learned.
|
|
261
315
|
*/
|
|
262
316
|
async getResources(offset, limit) {
|
|
263
|
-
const
|
|
317
|
+
const all = await this.getInternalResources();
|
|
318
|
+
const internal = all.slice(offset, offset + limit);
|
|
264
319
|
const remaining = limit - internal.length;
|
|
265
320
|
if (remaining <= 0) {
|
|
266
321
|
return internal;
|
|
267
322
|
}
|
|
268
|
-
const externalOffset = Math.max(0, offset -
|
|
323
|
+
const externalOffset = Math.max(0, offset - all.length);
|
|
269
324
|
return [...internal, ...await this.getExternalResources(externalOffset, remaining)];
|
|
270
325
|
}
|
|
271
326
|
}
|
|
@@ -22,4 +22,9 @@ export interface PaymentRefusal {
|
|
|
22
22
|
* scheduled after returning, so it doesn't delay the caller, and failures go to `onSettleError`.
|
|
23
23
|
*/
|
|
24
24
|
export declare function settlePaidCall(operatorClient: OperatorClient, voucher: PaymentVoucher | InferencePaymentVoucher, call: () => Promise<InvokeResult>, onSettleError: (error: unknown, message: string) => void): Promise<InvokeResult | PaymentRefusal>;
|
|
25
|
+
/**
|
|
26
|
+
* Captures a billed run with the tokens it executed, or releases it, after the response: nothing waits on
|
|
27
|
+
* settlement.
|
|
28
|
+
*/
|
|
29
|
+
export declare function settleRunLater(operatorClient: OperatorClient, runId: string, outcome: 'capture' | 'release', tokens: number, onSettleError: (error: unknown, message: string) => void): void;
|
|
25
30
|
//# sourceMappingURL=settlement.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settlement.d.ts","sourceRoot":"","sources":["../../src/resource/settlement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,6BAA6B,CAAC;AACrF,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,KAAK,uBAAuB,EAAsB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAErG,sFAAsF;AACtF,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClC,KAAK,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAYD;;;;GAIG;AACH,wBAAsB,cAAc,CAChC,cAAc,EAAE,cAAc,EAC9B,OAAO,EAAE,cAAc,GAAG,uBAAuB,EACjD,IAAI,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,EACjC,aAAa,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GACzD,OAAO,CAAC,YAAY,GAAG,cAAc,CAAC,CAuCxC"}
|
|
1
|
+
{"version":3,"file":"settlement.d.ts","sourceRoot":"","sources":["../../src/resource/settlement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,6BAA6B,CAAC;AACrF,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,KAAK,uBAAuB,EAAsB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAErG,sFAAsF;AACtF,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAClC,KAAK,CAAC,EAAE,cAAc,CAAC;CAC1B;AAED,wEAAwE;AACxE,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAYD;;;;GAIG;AACH,wBAAsB,cAAc,CAChC,cAAc,EAAE,cAAc,EAC9B,OAAO,EAAE,cAAc,GAAG,uBAAuB,EACjD,IAAI,EAAE,MAAM,OAAO,CAAC,YAAY,CAAC,EACjC,aAAa,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GACzD,OAAO,CAAC,YAAY,GAAG,cAAc,CAAC,CAuCxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC1B,cAAc,EAAE,cAAc,EAC9B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,SAAS,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GACzD,IAAI,CAYN"}
|
|
@@ -57,3 +57,22 @@ export async function settlePaidCall(operatorClient, voucher, call, onSettleErro
|
|
|
57
57
|
settle();
|
|
58
58
|
return result;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Captures a billed run with the tokens it executed, or releases it, after the response: nothing waits on
|
|
62
|
+
* settlement.
|
|
63
|
+
*/
|
|
64
|
+
export function settleRunLater(operatorClient, runId, outcome, tokens, onSettleError) {
|
|
65
|
+
setTimeout(async () => {
|
|
66
|
+
try {
|
|
67
|
+
if (outcome === 'capture') {
|
|
68
|
+
await operatorClient.captureRun({ runId, tokens });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await operatorClient.releaseRun({ runId });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
onSettleError(error, `Failed to ${outcome} run ${runId}`);
|
|
76
|
+
}
|
|
77
|
+
}, 0);
|
|
78
|
+
}
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAEhF,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAEhF,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQ1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAK5C,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED,UAAU,YAAY;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,eAAe,CAAC;CAC/B;AAsCD;;;GAGG;AACH,KAAK,SAAS,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,cAAc,CAAC,CAAC;AAE3F,OAAO,QAAQ,SAAS,CAAC;IACrB,UAAU,cAAc;QACpB,SAAS,CAAC,EAAE,SAAS,CAAC;KACzB;CACJ;AAkBD,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,aAAa,MAAM,CAAC;AAkOjC;;;;GAIG;AACH,QAAA,MAAM,MAAM,EAAE,kBAAkB,CAAC,YAAY,CAigB5C,CAAC;eAGa,MAAM"}
|
package/dist/routes.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import rateLimit from '@fastify/rate-limit';
|
|
3
|
-
import { interpretParsed, parseScript, RequestScriptError, TokenMeter } from 'requestscript';
|
|
3
|
+
import { decodeValue, interpretParsed, parseScript, RequestScriptError, TokenMeter, valueToJson } from 'requestscript';
|
|
4
4
|
import { looksLikeJwt } from './client/runTokens.js';
|
|
5
|
-
import { inferenceVoucherPayload, invocationHash, isInferenceVoucher, paymentVoucherPayload } from './resource/invoker.js';
|
|
5
|
+
import { contractVoucherPayload, inferenceVoucherPayload, invocationHash, isContractVoucher, isInferenceVoucher, MAX_CONTRACT_DEPTH, paymentVoucherPayload } from './resource/invoker.js';
|
|
6
6
|
import { estimateInputTokens, inferenceCharge, isInferenceFunction, isInferenceManifest, parseMaxTokens } from './resource/inference.js';
|
|
7
|
-
import { settlePaidCall } from './resource/settlement.js';
|
|
7
|
+
import { settlePaidCall, settleRunLater } from './resource/settlement.js';
|
|
8
|
+
import { isContractFunction, isContractManifest } from './resource/repository.js';
|
|
8
9
|
import { safeVerifyData } from './crypto/utils.js';
|
|
9
10
|
import { InvalidPublicKeyError } from './grant/service.js';
|
|
10
11
|
// Body schemas check only the shape the handlers rely on. Signed payloads (resources) are
|
|
@@ -94,15 +95,37 @@ const inferenceVoucherSchema = {
|
|
|
94
95
|
publicKey: { type: 'string' },
|
|
95
96
|
},
|
|
96
97
|
};
|
|
98
|
+
const contractVoucherSchema = {
|
|
99
|
+
type: 'object',
|
|
100
|
+
required: ['kind', 'requestId', 'invocationId', 'maxAmount', 'priceMicroUsdPerMillionTokens', 'recipientOrganizationId', 'signature', 'expiresAt', 'invocationHash', 'publicKey'],
|
|
101
|
+
properties: {
|
|
102
|
+
kind: { const: 'contract' },
|
|
103
|
+
requestId: { type: 'string' },
|
|
104
|
+
invocationId: { type: 'string' },
|
|
105
|
+
maxAmount: { type: 'integer', minimum: 0 },
|
|
106
|
+
priceMicroUsdPerMillionTokens: { type: 'integer', minimum: 0 },
|
|
107
|
+
recipientOrganizationId: { type: 'string' },
|
|
108
|
+
signature: { type: 'string' },
|
|
109
|
+
expiresAt: { type: 'string' },
|
|
110
|
+
invocationHash: { type: 'string' },
|
|
111
|
+
publicKey: { type: 'string' },
|
|
112
|
+
},
|
|
113
|
+
};
|
|
97
114
|
const resourceRunRequestSchema = {
|
|
98
115
|
type: 'object',
|
|
99
116
|
required: ['resourcePath', 'resourceName', 'functionName', 'parameters'],
|
|
100
117
|
properties: {
|
|
101
118
|
...resourceCallProperties,
|
|
102
|
-
//
|
|
103
|
-
paymentVoucher: { oneOf: [fixedVoucherSchema, inferenceVoucherSchema] },
|
|
119
|
+
// Inference and contract vouchers have their own `kind`, and no `amount`, so at most one shape matches.
|
|
120
|
+
paymentVoucher: { oneOf: [fixedVoucherSchema, inferenceVoucherSchema, contractVoucherSchema] },
|
|
121
|
+
contractDepth: { type: 'integer', minimum: 1 },
|
|
104
122
|
},
|
|
105
123
|
};
|
|
124
|
+
const contractRequestSchema = {
|
|
125
|
+
type: 'object',
|
|
126
|
+
required: ['source'],
|
|
127
|
+
properties: { source: { type: 'string' }, description: { type: 'string', maxLength: 2048 } },
|
|
128
|
+
};
|
|
106
129
|
const resourceEstimateRequestSchema = {
|
|
107
130
|
type: 'object',
|
|
108
131
|
required: ['resourcePath', 'resourceName', 'functionName', 'parameters'],
|
|
@@ -162,7 +185,7 @@ function isValidPaymentVoucher(voucher, resourceFunction, manifest, request, org
|
|
|
162
185
|
}
|
|
163
186
|
return safeVerifyData(inferenceVoucherPayload(voucher), voucher.publicKey, voucher.signature);
|
|
164
187
|
}
|
|
165
|
-
if (isInferenceFunction(resourceFunction) || isInferenceManifest(manifest)) {
|
|
188
|
+
if (isInferenceFunction(resourceFunction) || isInferenceManifest(manifest) || isContractFunction(resourceFunction) || isContractManifest(manifest)) {
|
|
166
189
|
return false;
|
|
167
190
|
}
|
|
168
191
|
if (voucher.amount < manifest.costMicroUsd) {
|
|
@@ -170,6 +193,37 @@ function isValidPaymentVoucher(voucher, resourceFunction, manifest, request, org
|
|
|
170
193
|
}
|
|
171
194
|
return safeVerifyData(paymentVoucherPayload(voucher), voucher.publicKey, voucher.signature);
|
|
172
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* A contract voucher is valid when it is bound to this exact call, signed by its public key, unexpired, pays
|
|
198
|
+
* this node's organization, and agrees to at least this node's current price.
|
|
199
|
+
*/
|
|
200
|
+
function isValidContractVoucher(voucher, request, organizationId, price) {
|
|
201
|
+
const expectedHash = invocationHash(request.resourcePath, request.resourceName, request.functionName, request.parameters);
|
|
202
|
+
if (voucher.invocationHash !== expectedHash || !organizationId || voucher.recipientOrganizationId !== organizationId) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
const expiresAt = Date.parse(voucher.expiresAt);
|
|
206
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now() || voucher.priceMicroUsdPerMillionTokens < price) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
return safeVerifyData(contractVoucherPayload(voucher), voucher.publicKey, voucher.signature);
|
|
210
|
+
}
|
|
211
|
+
/** Splits `path.Name` or `path.Name@version`. */
|
|
212
|
+
function contractRef(ref) {
|
|
213
|
+
const at = ref.indexOf('@');
|
|
214
|
+
const qualified = at === -1 ? ref : ref.slice(0, at);
|
|
215
|
+
const version = at === -1 ? undefined : ref.slice(at + 1);
|
|
216
|
+
const dot = qualified.lastIndexOf('.');
|
|
217
|
+
if (dot <= 0 || dot === qualified.length - 1 || version === '') {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return { path: qualified.slice(0, dot), name: qualified.slice(dot + 1), version };
|
|
221
|
+
}
|
|
222
|
+
/** A contract as `GET /v1/contracts` lists it. Its source is public, so callers can read what a call does. */
|
|
223
|
+
function contractView(contract) {
|
|
224
|
+
const { path, name, version, parameters, returnType, ownerOrganizationId, description, source, createdAt } = contract;
|
|
225
|
+
return { path, name, version, parameters, returnType, ownerOrganizationId, description, source, createdAt };
|
|
226
|
+
}
|
|
173
227
|
/**
|
|
174
228
|
* Encapsulates the routes
|
|
175
229
|
* @param fastify Encapsulated Fastify Instance
|
|
@@ -262,7 +316,10 @@ const routes = async (fastify, options) => {
|
|
|
262
316
|
type: parameter.type,
|
|
263
317
|
description: parameter.description,
|
|
264
318
|
})),
|
|
265
|
-
paymentManifest: !fn.paymentManifest ? undefined :
|
|
319
|
+
paymentManifest: !fn.paymentManifest ? undefined : isContractManifest(fn.paymentManifest) ? {
|
|
320
|
+
priceMicroUsdPerMillionTokens: fn.paymentManifest.priceMicroUsdPerMillionTokens,
|
|
321
|
+
recipientOrganizationId: fn.paymentManifest.recipientOrganizationId,
|
|
322
|
+
} : isInferenceManifest(fn.paymentManifest) ? {
|
|
266
323
|
inputCostMicroUsdPerMillionTokens: fn.paymentManifest.inputCostMicroUsdPerMillionTokens,
|
|
267
324
|
outputCostMicroUsdPerMillionTokens: fn.paymentManifest.outputCostMicroUsdPerMillionTokens,
|
|
268
325
|
recipientOrganizationId: fn.paymentManifest.recipientOrganizationId,
|
|
@@ -271,7 +328,7 @@ const routes = async (fastify, options) => {
|
|
|
271
328
|
recipientOrganizationId: fn.paymentManifest.recipientOrganizationId,
|
|
272
329
|
},
|
|
273
330
|
restricted: fn.restricted || undefined,
|
|
274
|
-
kind: isInferenceFunction(fn) ?
|
|
331
|
+
kind: isInferenceFunction(fn) || isContractFunction(fn) ? fn.kind : undefined,
|
|
275
332
|
})),
|
|
276
333
|
baseUrl: resource.baseUrl,
|
|
277
334
|
})),
|
|
@@ -317,36 +374,28 @@ const routes = async (fastify, options) => {
|
|
|
317
374
|
const caller = request.runCaller;
|
|
318
375
|
const billed = caller.kind === 'client' && caller.organizationId !== options.service.config.organizationId;
|
|
319
376
|
const runId = billed ? randomUUID() : undefined;
|
|
320
|
-
let resourceInvoker = options.service.resourceInvoker;
|
|
321
377
|
if (billed) {
|
|
322
378
|
const authorization = await options.service.operatorClient.authorizeRun({ token: caller.token, runId: runId, source: runRequest.source });
|
|
323
379
|
if (!authorization.ok) {
|
|
324
380
|
return reply.status(authorization.status).send({ error: authorization.error });
|
|
325
381
|
}
|
|
326
|
-
resourceInvoker = options.service.resourceInvoker.forRun({ runId: runId });
|
|
327
382
|
}
|
|
383
|
+
// The run token's claims, or null for an API-key run, so the name is always reserved.
|
|
384
|
+
const jwt = caller.kind === 'client' ? caller.payload : null;
|
|
385
|
+
// Contracts the script calls on this node see who pays for the run: the client's organization, or this node's own.
|
|
386
|
+
const { organizationId, publicKey } = options.service.config;
|
|
387
|
+
const runFor = { org: caller.kind === 'client' ? caller.organizationId : organizationId ?? null, node: publicKey };
|
|
388
|
+
const resourceInvoker = options.service.resourceInvoker.forScript({ runId, jwt, depth: 0, caller: runFor });
|
|
328
389
|
// Nothing waits on settlement, so it doesn't delay the response.
|
|
329
|
-
const settle = (outcome, tokens) => runId
|
|
330
|
-
|
|
331
|
-
if (outcome === 'capture') {
|
|
332
|
-
await options.service.operatorClient.captureRun({ runId, tokens });
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
await options.service.operatorClient.releaseRun({ runId });
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
catch (error) {
|
|
339
|
-
fastify.log.error(error, `Failed to ${outcome} run ${runId}`);
|
|
340
|
-
}
|
|
341
|
-
}, 0);
|
|
390
|
+
const settle = (outcome, tokens) => runId
|
|
391
|
+
&& settleRunLater(options.service.operatorClient, runId, outcome, tokens, (error, message) => fastify.log.error(error, message));
|
|
342
392
|
const meter = new TokenMeter();
|
|
343
393
|
try {
|
|
344
394
|
const result = await interpretParsed(script, {
|
|
345
395
|
resourceResolver: options.service.resourceResolver,
|
|
346
396
|
resourceInvoker,
|
|
347
397
|
meter,
|
|
348
|
-
|
|
349
|
-
builtins: { jwt: caller.kind === 'client' ? caller.payload : null },
|
|
398
|
+
builtins: { jwt },
|
|
350
399
|
});
|
|
351
400
|
settle('capture', meter.tokens);
|
|
352
401
|
return reply.status(200).send({ returnValue: result.returnValue ?? null, usage: { tokens: meter.tokens } });
|
|
@@ -442,6 +491,35 @@ const routes = async (fastify, options) => {
|
|
|
442
491
|
return reply;
|
|
443
492
|
}
|
|
444
493
|
const { resource, resourceFunction } = call;
|
|
494
|
+
// A contract is always paid for with a contract voucher, since its paid calls are paid by its caller.
|
|
495
|
+
if (isContractFunction(resourceFunction)) {
|
|
496
|
+
const contract = await options.service.contractRepository.latest(resource.path, resource.name);
|
|
497
|
+
if (!contract) {
|
|
498
|
+
return reply.status(400).send({ error: 'Resource not found' });
|
|
499
|
+
}
|
|
500
|
+
if (!isContractVoucher(paymentVoucher)) {
|
|
501
|
+
return reply.status(402).send({ error: 'A contract voucher is required' });
|
|
502
|
+
}
|
|
503
|
+
const { organizationId, priceMicroUsdPerMillionTokens } = options.service.config;
|
|
504
|
+
if (!isValidContractVoucher(paymentVoucher, resourceRequest, organizationId, priceMicroUsdPerMillionTokens ?? 0)) {
|
|
505
|
+
return reply.status(400).send({ error: 'Invalid payment voucher' });
|
|
506
|
+
}
|
|
507
|
+
if (await options.service.grantService.isBlocked(paymentVoucher.publicKey)) {
|
|
508
|
+
return reply.status(403).send({ error: 'Caller is blocked' });
|
|
509
|
+
}
|
|
510
|
+
const depth = resourceRequest.contractDepth ?? 1;
|
|
511
|
+
if (depth > MAX_CONTRACT_DEPTH) {
|
|
512
|
+
return reply.status(400).send({ error: `Contracts may call each other at most ${MAX_CONTRACT_DEPTH} deep` });
|
|
513
|
+
}
|
|
514
|
+
const result = await options.service.contractService.serve(paymentVoucher, contract, resourceRequest.parameters, depth);
|
|
515
|
+
if ('refused' in result) {
|
|
516
|
+
return reply.status(result.refused.status).send({ error: result.refused.error });
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
if (isContractVoucher(paymentVoucher)) {
|
|
521
|
+
return reply.status(400).send({ error: 'Invalid payment voucher' });
|
|
522
|
+
}
|
|
445
523
|
// Payment manifests are per function. A paid function is never run without a valid voucher.
|
|
446
524
|
const paymentManifest = resourceFunction.paymentManifest;
|
|
447
525
|
if (paymentManifest) {
|
|
@@ -478,6 +556,64 @@ const routes = async (fastify, options) => {
|
|
|
478
556
|
return result;
|
|
479
557
|
});
|
|
480
558
|
// End Run Routes
|
|
559
|
+
// Start Contract Routes
|
|
560
|
+
/** The node's admin, or an organization's API client, from `requireRunAuthorization`. */
|
|
561
|
+
const contractCaller = (request) => {
|
|
562
|
+
const caller = request.runCaller;
|
|
563
|
+
return caller.kind === 'apiKey' ? caller : { kind: 'client', token: caller.token, organizationId: caller.organizationId };
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* Deploys a version of a contract, which becomes one of this node's resources. Another organization's
|
|
567
|
+
* API client pays the node's deploy fee from its run hold.
|
|
568
|
+
*/
|
|
569
|
+
fastify.post('/contracts', { schema: { body: contractRequestSchema }, preValidation: requireRunAuthorization }, async (request, reply) => {
|
|
570
|
+
const { source, description } = request.body;
|
|
571
|
+
const result = await options.service.contractService.deploy(source, description, contractCaller(request));
|
|
572
|
+
if (!result.ok) {
|
|
573
|
+
return reply.status(result.status).send({ error: result.error });
|
|
574
|
+
}
|
|
575
|
+
return reply.status(201).send(result.contract);
|
|
576
|
+
});
|
|
577
|
+
/** Every version of every contract on this node, with its source. */
|
|
578
|
+
fastify.get('/contracts', { schema: { querystring: pageQuerySchema } }, async (request) => {
|
|
579
|
+
const { items, nextOffset } = await getPage(request.query, (offset, limit) => options.service.contractRepository.list(offset, limit));
|
|
580
|
+
return { contracts: items.map(contractView), nextOffset };
|
|
581
|
+
});
|
|
582
|
+
/**
|
|
583
|
+
* A contract's state variables, every scope, with values as JSON. Only its owner and this node's admin may
|
|
584
|
+
* read them: a contract shares its state with callers through what it returns.
|
|
585
|
+
*/
|
|
586
|
+
fastify.get('/contracts/:ref/state', { preValidation: requireRunAuthorization }, async (request, reply) => {
|
|
587
|
+
const ref = contractRef(request.params.ref);
|
|
588
|
+
if (!ref || ref.version !== undefined) {
|
|
589
|
+
return reply.status(400).send({ error: 'Name a contract as path.Name: state belongs to every version' });
|
|
590
|
+
}
|
|
591
|
+
const result = await options.service.contractService.readState(ref.path, ref.name, contractCaller(request));
|
|
592
|
+
if (!result.ok) {
|
|
593
|
+
return reply.status(result.status).send({ error: result.error });
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
variables: result.variables.map(({ variable, scope, value, updatedAt }) => ({
|
|
597
|
+
name: variable,
|
|
598
|
+
scope,
|
|
599
|
+
value: valueToJson(decodeValue(value)),
|
|
600
|
+
updatedAt,
|
|
601
|
+
})),
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
/** Deletes `path.Name@version`, or every version of `path.Name`. */
|
|
605
|
+
fastify.delete('/contracts/:ref', { preValidation: requireRunAuthorization }, async (request, reply) => {
|
|
606
|
+
const ref = contractRef(request.params.ref);
|
|
607
|
+
if (!ref) {
|
|
608
|
+
return reply.status(400).send({ error: 'Name a contract as path.Name or path.Name@version' });
|
|
609
|
+
}
|
|
610
|
+
const result = await options.service.contractService.delete(ref.path, ref.name, ref.version, contractCaller(request));
|
|
611
|
+
if (!result.ok) {
|
|
612
|
+
return reply.status(result.status).send({ error: result.error });
|
|
613
|
+
}
|
|
614
|
+
return { success: true };
|
|
615
|
+
});
|
|
616
|
+
// End Contract Routes
|
|
481
617
|
// Start Grant Routes
|
|
482
618
|
/** Issues a root grant to the request's signer, if the node's approval hook allows it. */
|
|
483
619
|
fastify.post('/grants/request', {
|