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,3 +1,4 @@
|
|
|
1
|
+
import type { ContractPaymentVoucher } from "../resource/invoker.js";
|
|
1
2
|
export type PaymentSubmission = FixedPaymentSubmission | InferencePaymentSubmission;
|
|
2
3
|
export interface FixedPaymentVoucher {
|
|
3
4
|
kind?: undefined;
|
|
@@ -52,6 +53,33 @@ export interface RunAuthorizationRequest {
|
|
|
52
53
|
runId: string;
|
|
53
54
|
source: string;
|
|
54
55
|
}
|
|
56
|
+
/** What a node sends to authorize a call to one of its contracts. Matches apps/operator/src/runs/runService.ts. */
|
|
57
|
+
export interface ContractRunAuthorizationRequest {
|
|
58
|
+
/** The caller's voucher. Its `invocationId` is the run's id. */
|
|
59
|
+
voucher: ContractPaymentVoucher;
|
|
60
|
+
/** The contract's source, which the operator prices. */
|
|
61
|
+
source: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The operator's answer to a contract call: a hold for the run named by the voucher's `invocationId`, or
|
|
65
|
+
* `free` when the caller is in this node's organization, which doesn't pay itself. `payerOrganizationId` is
|
|
66
|
+
* the organization paying for the call, which the contract sees as `caller.org`. Older operators omit it.
|
|
67
|
+
*/
|
|
68
|
+
export type ContractRunAuthorizeResult = {
|
|
69
|
+
ok: true;
|
|
70
|
+
free: boolean;
|
|
71
|
+
payerOrganizationId?: string;
|
|
72
|
+
} | {
|
|
73
|
+
ok: false;
|
|
74
|
+
status: number;
|
|
75
|
+
error: string;
|
|
76
|
+
};
|
|
77
|
+
/** What the operator knows of this node. */
|
|
78
|
+
export interface OperatorSession {
|
|
79
|
+
organizationId: string;
|
|
80
|
+
/** The node's listed price per million script tokens. Absent from older operators. */
|
|
81
|
+
priceMicroUsdPerMillionTokens?: number;
|
|
82
|
+
}
|
|
55
83
|
/** The operator's hold for a run: up to `heldMicroUsd` for the script's tokens and its paid calls together. */
|
|
56
84
|
export type RunAuthorizeResult = {
|
|
57
85
|
ok: true;
|
|
@@ -86,9 +114,7 @@ export declare class OperatorClient {
|
|
|
86
114
|
* enrolls the node first; a node that is already enrolled ignores the token. Throws if the node is
|
|
87
115
|
* not enrolled or the operator refuses.
|
|
88
116
|
*/
|
|
89
|
-
session(enrollmentToken?: string, baseUrl?: string): Promise<
|
|
90
|
-
organizationId: string;
|
|
91
|
-
}>;
|
|
117
|
+
session(enrollmentToken?: string, baseUrl?: string): Promise<OperatorSession>;
|
|
92
118
|
/**
|
|
93
119
|
* Asks the operator to hold the voucher's amount (or `maxAmount`) on the payer's balance, before
|
|
94
120
|
* running the paid function. An unreachable operator is reported as a 503 refusal.
|
|
@@ -99,6 +125,11 @@ export declare class OperatorClient {
|
|
|
99
125
|
* it. An unreachable operator is reported as a 503 refusal.
|
|
100
126
|
*/
|
|
101
127
|
authorizeRun(request: RunAuthorizationRequest): Promise<RunAuthorizeResult>;
|
|
128
|
+
/**
|
|
129
|
+
* Asks the operator to hold a contract call's cost on the caller's organization, or on the caller's run,
|
|
130
|
+
* before running the contract. An unreachable operator is reported as a 503 refusal.
|
|
131
|
+
*/
|
|
132
|
+
authorizeContractRun(request: ContractRunAuthorizationRequest): Promise<ContractRunAuthorizeResult>;
|
|
102
133
|
/** Charges a held run for the tokens its script executed, and releases the rest of its hold. */
|
|
103
134
|
captureRun(request: {
|
|
104
135
|
runId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operatorClient.d.ts","sourceRoot":"","sources":["../../src/client/operatorClient.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"operatorClient.d.ts","sourceRoot":"","sources":["../../src/client/operatorClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -103,6 +103,21 @@ export class OperatorClient {
|
|
|
103
103
|
const { heldMicroUsd, maxTokens } = result.body;
|
|
104
104
|
return { ok: true, heldMicroUsd, maxTokens };
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Asks the operator to hold a contract call's cost on the caller's organization, or on the caller's run,
|
|
108
|
+
* before running the contract. An unreachable operator is reported as a 503 refusal.
|
|
109
|
+
*/
|
|
110
|
+
async authorizeContractRun(request) {
|
|
111
|
+
const result = await this.hold('/runs/contract/authorize', request);
|
|
112
|
+
if (!result.ok)
|
|
113
|
+
return result;
|
|
114
|
+
const body = result.body;
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
free: body?.status === 'free',
|
|
118
|
+
...(typeof body?.payerOrganizationId === 'string' ? { payerOrganizationId: body.payerOrganizationId } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
106
121
|
/** Charges a held run for the tokens its script executed, and releases the rest of its hold. */
|
|
107
122
|
async captureRun(request) {
|
|
108
123
|
await this.settle('/runs/capture', request);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { NodeDatabase } from "../db/database.js";
|
|
2
|
+
export interface ContractParameter {
|
|
3
|
+
name: string;
|
|
4
|
+
/** A RequestScript type string, e.g. 'int32' or '[]string'. */
|
|
5
|
+
type: string;
|
|
6
|
+
}
|
|
7
|
+
export interface StoredContract {
|
|
8
|
+
path: string;
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
source: string;
|
|
12
|
+
parameters: ContractParameter[];
|
|
13
|
+
returnType: string;
|
|
14
|
+
/** The organization that deployed it, which owns every version. Null when the node's admin did. */
|
|
15
|
+
ownerOrganizationId: string | null;
|
|
16
|
+
description: string | null;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
/** Why `save` refused a contract. */
|
|
20
|
+
export type SaveContractRefusal = 'version_exists' | 'owned_by_another';
|
|
21
|
+
/**
|
|
22
|
+
* Contracts deployed to this node. Versions are immutable, and every version of a contract belongs to the
|
|
23
|
+
* organization that deployed its first one.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ContractRepository {
|
|
26
|
+
private readonly db;
|
|
27
|
+
constructor(db: NodeDatabase);
|
|
28
|
+
/**
|
|
29
|
+
* Saves a new version. `ownerOrganizationId` null is the node's admin, which may add versions to any
|
|
30
|
+
* contract; they stay owned by the contract's owner.
|
|
31
|
+
*/
|
|
32
|
+
save(contract: Omit<StoredContract, 'createdAt'>): Promise<{
|
|
33
|
+
ok: true;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
refused: SaveContractRefusal;
|
|
37
|
+
}>;
|
|
38
|
+
/** The contract's latest version. */
|
|
39
|
+
latest(path: string, name: string): Promise<StoredContract | undefined>;
|
|
40
|
+
/** The owner of the contract at `path.name`: an organization, null for the node's admin, or undefined if there is none. */
|
|
41
|
+
owner(path: string, name: string): Promise<string | null | undefined>;
|
|
42
|
+
/** The latest version of every contract, ordered by path and name. */
|
|
43
|
+
allLatest(): Promise<StoredContract[]>;
|
|
44
|
+
/** One page of every version, ordered by path, name and deploy time. */
|
|
45
|
+
list(offset: number, limit: number): Promise<StoredContract[]>;
|
|
46
|
+
/** Deletes one version, or every version when `version` is omitted. False when there was nothing to delete. */
|
|
47
|
+
delete(path: string, name: string, version?: string): Promise<boolean>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../../src/contract/repository.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,mGAAmG;IACnG,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;CACrB;AAyCD,qCAAqC;AACrC,MAAM,MAAM,mBAAmB,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;AAExE;;;GAGG;AACH,qBAAa,kBAAkB;IACf,OAAO,CAAC,QAAQ,CAAC,EAAE;IAA/B,YAA6B,EAAE,EAAE,YAAY,EAAI;IAEjD;;;OAGG;IACG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,OAAO,EAAE,mBAAmB,CAAA;KAAE,CAAC,CAgB3H;IAED,qCAAqC;IAC/B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAG5E;IAED,2HAA2H;IACrH,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAG1E;IAED,sEAAsE;IAChE,SAAS,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CAE3C;IAED,wEAAwE;IAClE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAGnE;IAED,+GAA+G;IACzG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAK3E;CACJ"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { compareVersions } from "requestscript";
|
|
2
|
+
function toContract(row) {
|
|
3
|
+
return {
|
|
4
|
+
path: row.path,
|
|
5
|
+
name: row.name,
|
|
6
|
+
version: row.version,
|
|
7
|
+
source: row.source,
|
|
8
|
+
parameters: JSON.parse(row.parameters),
|
|
9
|
+
returnType: row.return_type,
|
|
10
|
+
ownerOrganizationId: row.owner_organization_id,
|
|
11
|
+
description: row.description,
|
|
12
|
+
createdAt: row.created_at,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** The latest version of each contract in `rows`, ordered by path and name. */
|
|
16
|
+
function latestOf(rows) {
|
|
17
|
+
const latest = new Map();
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
const key = `${row.path}.${row.name}`;
|
|
20
|
+
const current = latest.get(key);
|
|
21
|
+
if (!current || compareVersions(row.version, current.version) > 0) {
|
|
22
|
+
latest.set(key, row);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [...latest.values()].map(toContract);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Contracts deployed to this node. Versions are immutable, and every version of a contract belongs to the
|
|
29
|
+
* organization that deployed its first one.
|
|
30
|
+
*/
|
|
31
|
+
export class ContractRepository {
|
|
32
|
+
db;
|
|
33
|
+
constructor(db) {
|
|
34
|
+
this.db = db;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Saves a new version. `ownerOrganizationId` null is the node's admin, which may add versions to any
|
|
38
|
+
* contract; they stay owned by the contract's owner.
|
|
39
|
+
*/
|
|
40
|
+
async save(contract) {
|
|
41
|
+
return this.db.transaction(async (tx) => {
|
|
42
|
+
const existing = await tx.all('SELECT * FROM contract WHERE path = ? AND name = ?', [contract.path, contract.name]);
|
|
43
|
+
const owner = existing.length > 0 ? existing[0].owner_organization_id : contract.ownerOrganizationId;
|
|
44
|
+
if (contract.ownerOrganizationId !== null && owner !== contract.ownerOrganizationId) {
|
|
45
|
+
return { ok: false, refused: 'owned_by_another' };
|
|
46
|
+
}
|
|
47
|
+
if (existing.some(row => row.version === contract.version)) {
|
|
48
|
+
return { ok: false, refused: 'version_exists' };
|
|
49
|
+
}
|
|
50
|
+
await tx.run('INSERT INTO contract (path, name, version, source, parameters, return_type, owner_organization_id, description, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [contract.path, contract.name, contract.version, contract.source, JSON.stringify(contract.parameters), contract.returnType, owner, contract.description, new Date().toISOString()]);
|
|
51
|
+
return { ok: true };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** The contract's latest version. */
|
|
55
|
+
async latest(path, name) {
|
|
56
|
+
const rows = await this.db.all('SELECT * FROM contract WHERE path = ? AND name = ?', [path, name]);
|
|
57
|
+
return latestOf(rows)[0];
|
|
58
|
+
}
|
|
59
|
+
/** The owner of the contract at `path.name`: an organization, null for the node's admin, or undefined if there is none. */
|
|
60
|
+
async owner(path, name) {
|
|
61
|
+
const row = await this.db.get('SELECT owner_organization_id FROM contract WHERE path = ? AND name = ? LIMIT 1', [path, name]);
|
|
62
|
+
return row ? row.owner_organization_id : undefined;
|
|
63
|
+
}
|
|
64
|
+
/** The latest version of every contract, ordered by path and name. */
|
|
65
|
+
async allLatest() {
|
|
66
|
+
return latestOf(await this.db.all('SELECT * FROM contract ORDER BY path, name'));
|
|
67
|
+
}
|
|
68
|
+
/** One page of every version, ordered by path, name and deploy time. */
|
|
69
|
+
async list(offset, limit) {
|
|
70
|
+
const rows = await this.db.all('SELECT * FROM contract ORDER BY path, name, created_at, version LIMIT ? OFFSET ?', [limit, offset]);
|
|
71
|
+
return rows.map(toContract);
|
|
72
|
+
}
|
|
73
|
+
/** Deletes one version, or every version when `version` is omitted. False when there was nothing to delete. */
|
|
74
|
+
async delete(path, name, version) {
|
|
75
|
+
const result = version === undefined
|
|
76
|
+
? await this.db.run('DELETE FROM contract WHERE path = ? AND name = ?', [path, name])
|
|
77
|
+
: await this.db.run('DELETE FROM contract WHERE path = ? AND name = ? AND version = ?', [path, name, version]);
|
|
78
|
+
return result.changes > 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type Resource, type ResourceFunctionCallParameter, type ResourceResolver } from "requestscript";
|
|
2
|
+
import type { OperatorClient } from "../client/operatorClient.js";
|
|
3
|
+
import { type CombinationResourceInvoker, type ContractHost, type ContractPaymentVoucher, type ScriptContext } from "../resource/invoker.js";
|
|
4
|
+
import { type ResourceRepository } from "../resource/repository.js";
|
|
5
|
+
import { type InvokeResult } from "../resource/settlement.js";
|
|
6
|
+
import type { NodeConfig } from "../service.js";
|
|
7
|
+
import type { ContractRepository, StoredContract } from "./repository.js";
|
|
8
|
+
import { type ContractStateRepository, type ContractStateRow } from "./state.js";
|
|
9
|
+
export declare const DEFAULT_MAX_CONTRACT_SOURCE_BYTES: number;
|
|
10
|
+
/** The longest a call waits for earlier calls of the same stateful contract on this node. */
|
|
11
|
+
export declare const CONTRACT_LOCK_TIMEOUT_MS = 30000;
|
|
12
|
+
/** Who deploys or deletes a contract: the node's admin with its API key, or an organization's API client with a run token. */
|
|
13
|
+
export type ContractCaller = {
|
|
14
|
+
kind: 'apiKey';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'client';
|
|
17
|
+
token: string;
|
|
18
|
+
organizationId: string;
|
|
19
|
+
};
|
|
20
|
+
/** A refusal, with the HTTP status a route answers. */
|
|
21
|
+
export type ContractRefusal = {
|
|
22
|
+
ok: false;
|
|
23
|
+
status: number;
|
|
24
|
+
error: string;
|
|
25
|
+
};
|
|
26
|
+
export interface DeployedContract {
|
|
27
|
+
path: string;
|
|
28
|
+
name: string;
|
|
29
|
+
version: string;
|
|
30
|
+
/** What the deploy cost the caller's organization, in micro-USD. */
|
|
31
|
+
feeMicroUsd: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Deploys, deletes and runs the contracts on this node. A contract's latest version is one of the node's
|
|
35
|
+
* resources, with one function, `run`. Calls to it are billed by the tokens it executes, through a run the
|
|
36
|
+
* operator holds on the caller's organization or on the caller's own run, which also pays for the paid calls
|
|
37
|
+
* the contract makes.
|
|
38
|
+
*/
|
|
39
|
+
export declare class ContractService implements ContractHost {
|
|
40
|
+
private readonly contracts;
|
|
41
|
+
private readonly state;
|
|
42
|
+
private readonly resources;
|
|
43
|
+
private readonly resolver;
|
|
44
|
+
private readonly invoker;
|
|
45
|
+
private readonly operatorClient;
|
|
46
|
+
private readonly config;
|
|
47
|
+
private readonly log;
|
|
48
|
+
/** Tells peers about this node's resources again, after a contract is deployed or deleted. Set once the node listens. */
|
|
49
|
+
announce?: () => void;
|
|
50
|
+
/** The latest call holding each stateful contract (`path.Name`) on this node. */
|
|
51
|
+
private readonly locks;
|
|
52
|
+
/** How long a call waits for earlier calls of the same stateful contract. */
|
|
53
|
+
lockTimeoutMs: number;
|
|
54
|
+
constructor(contracts: ContractRepository, state: ContractStateRepository, resources: ResourceRepository, resolver: ResourceResolver, invoker: CombinationResourceInvoker, operatorClient: OperatorClient, config: NodeConfig, log?: (error: unknown, message: string) => void);
|
|
55
|
+
/**
|
|
56
|
+
* Saves a new version of a contract. A deploy by another organization's API client pays this node's
|
|
57
|
+
* deploy fee from the client's run hold, and is charged only if the contract is saved.
|
|
58
|
+
*/
|
|
59
|
+
deploy(source: string, description: string | undefined, caller: ContractCaller): Promise<{
|
|
60
|
+
ok: true;
|
|
61
|
+
contract: DeployedContract;
|
|
62
|
+
} | ContractRefusal>;
|
|
63
|
+
/** Deletes a version, or every version, of a contract. Its owner or the node's admin may. */
|
|
64
|
+
delete(path: string, name: string, version: string | undefined, caller: ContractCaller): Promise<{
|
|
65
|
+
ok: true;
|
|
66
|
+
} | ContractRefusal>;
|
|
67
|
+
/** The contract's state variables and their scopes, for its owner and the node's admin. */
|
|
68
|
+
readState(path: string, name: string, caller: ContractCaller): Promise<{
|
|
69
|
+
ok: true;
|
|
70
|
+
variables: ContractStateRow[];
|
|
71
|
+
} | ContractRefusal>;
|
|
72
|
+
/**
|
|
73
|
+
* Runs a contract for another node's call, paid by its voucher, which the route has checked. The operator
|
|
74
|
+
* holds the call's cost as a run named by the voucher's `invocationId`, unless the caller is in this
|
|
75
|
+
* node's organization. `depth` is how many contracts deep the call is. The contract sees the payer the
|
|
76
|
+
* operator authorized, and the voucher's key, as `caller`.
|
|
77
|
+
*/
|
|
78
|
+
serve(voucher: ContractPaymentVoucher, contract: StoredContract, parameters: ResourceFunctionCallParameter[], depth: number): Promise<InvokeResult | {
|
|
79
|
+
refused: {
|
|
80
|
+
status: number;
|
|
81
|
+
error: string;
|
|
82
|
+
};
|
|
83
|
+
}>;
|
|
84
|
+
invokeLocal(resource: Resource, parameters: ResourceFunctionCallParameter[], context: ScriptContext): Promise<InvokeResult>;
|
|
85
|
+
/** Runs a contract under a held run, then captures the tokens it executed, or releases the run if it couldn't run. */
|
|
86
|
+
private runBilled;
|
|
87
|
+
/**
|
|
88
|
+
* Runs a contract's script. Its calls are made at `context.depth`, and billed to `context.runId` when set.
|
|
89
|
+
* A script that fails is reported as the call's error, as a failing function is.
|
|
90
|
+
*
|
|
91
|
+
* A contract with state variables runs one call at a time on this node, and saves its state only if the
|
|
92
|
+
* run succeeds. A call that re-enters a contract already running in its own chain skips the lock, and
|
|
93
|
+
* whichever of the two saves second finds the state changed and fails, so no update is lost.
|
|
94
|
+
*/
|
|
95
|
+
private run;
|
|
96
|
+
/**
|
|
97
|
+
* Waits for the contract's previous calls on this node, then holds it until the returned release is
|
|
98
|
+
* called. Answers undefined if the wait passes `lockTimeoutMs`, which breaks a cycle of calls between
|
|
99
|
+
* nodes that leads back here.
|
|
100
|
+
*/
|
|
101
|
+
private lock;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/contract/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { interpretParsed, parseScript, RequestScriptError, TokenMeter, typeToString } from "requestscript";
|
|
3
|
+
import { MAX_CONTRACT_DEPTH } from "../resource/invoker.js";
|
|
4
|
+
import { CONTRACT_FUNCTION } from "../resource/repository.js";
|
|
5
|
+
import { settlePaidCall, settleRunLater } from "../resource/settlement.js";
|
|
6
|
+
import { DEFAULT_MAX_CONTRACT_STATE_BYTES } from "./state.js";
|
|
7
|
+
export const DEFAULT_MAX_CONTRACT_SOURCE_BYTES = 64 * 1024;
|
|
8
|
+
/** The longest a call waits for earlier calls of the same stateful contract on this node. */
|
|
9
|
+
export const CONTRACT_LOCK_TIMEOUT_MS = 30_000;
|
|
10
|
+
const refuse = (status, error) => ({ ok: false, status, error });
|
|
11
|
+
/**
|
|
12
|
+
* Deploys, deletes and runs the contracts on this node. A contract's latest version is one of the node's
|
|
13
|
+
* resources, with one function, `run`. Calls to it are billed by the tokens it executes, through a run the
|
|
14
|
+
* operator holds on the caller's organization or on the caller's own run, which also pays for the paid calls
|
|
15
|
+
* the contract makes.
|
|
16
|
+
*/
|
|
17
|
+
export class ContractService {
|
|
18
|
+
contracts;
|
|
19
|
+
state;
|
|
20
|
+
resources;
|
|
21
|
+
resolver;
|
|
22
|
+
invoker;
|
|
23
|
+
operatorClient;
|
|
24
|
+
config;
|
|
25
|
+
log;
|
|
26
|
+
/** Tells peers about this node's resources again, after a contract is deployed or deleted. Set once the node listens. */
|
|
27
|
+
announce;
|
|
28
|
+
/** The latest call holding each stateful contract (`path.Name`) on this node. */
|
|
29
|
+
locks = new Map();
|
|
30
|
+
/** How long a call waits for earlier calls of the same stateful contract. */
|
|
31
|
+
lockTimeoutMs = CONTRACT_LOCK_TIMEOUT_MS;
|
|
32
|
+
constructor(contracts, state, resources, resolver, invoker, operatorClient, config, log = (error, message) => console.error(message, error)) {
|
|
33
|
+
this.contracts = contracts;
|
|
34
|
+
this.state = state;
|
|
35
|
+
this.resources = resources;
|
|
36
|
+
this.resolver = resolver;
|
|
37
|
+
this.invoker = invoker;
|
|
38
|
+
this.operatorClient = operatorClient;
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.log = log;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Saves a new version of a contract. A deploy by another organization's API client pays this node's
|
|
44
|
+
* deploy fee from the client's run hold, and is charged only if the contract is saved.
|
|
45
|
+
*/
|
|
46
|
+
async deploy(source, description, caller) {
|
|
47
|
+
const maxBytes = this.config.maxContractSourceBytes ?? DEFAULT_MAX_CONTRACT_SOURCE_BYTES;
|
|
48
|
+
if (Buffer.byteLength(source) > maxBytes) {
|
|
49
|
+
return refuse(413, `Contracts may be at most ${maxBytes} bytes`);
|
|
50
|
+
}
|
|
51
|
+
let script;
|
|
52
|
+
try {
|
|
53
|
+
script = parseScript(source);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return refuse(400, `Invalid contract: ${error instanceof Error ? error.message : String(error)}`);
|
|
57
|
+
}
|
|
58
|
+
const declaration = script.declaration;
|
|
59
|
+
if (declaration.kind !== 'contract') {
|
|
60
|
+
return refuse(400, 'Invalid contract: deploy a contract script, not a request');
|
|
61
|
+
}
|
|
62
|
+
if (declaration.path.length === 0) {
|
|
63
|
+
return refuse(400, 'Invalid contract: give it a path, such as com.example.Name');
|
|
64
|
+
}
|
|
65
|
+
if (!declaration.returnType) {
|
|
66
|
+
return refuse(400, 'Invalid contract: declare its return type, such as contract com.example.Name(...): string');
|
|
67
|
+
}
|
|
68
|
+
const path = declaration.path.join('.');
|
|
69
|
+
const { name, version } = declaration;
|
|
70
|
+
if (this.resources.isRegistered({ path, name }) || await this.resources.isLearned(path, name)) {
|
|
71
|
+
return refuse(409, `${path}.${name} is already a resource on the network`);
|
|
72
|
+
}
|
|
73
|
+
const ownerOrganizationId = caller.kind === 'client' ? caller.organizationId : null;
|
|
74
|
+
const owner = await this.contracts.owner(path, name);
|
|
75
|
+
if (owner !== undefined && ownerOrganizationId !== null && owner !== ownerOrganizationId) {
|
|
76
|
+
return refuse(409, `${path}.${name} belongs to another organization`);
|
|
77
|
+
}
|
|
78
|
+
const save = () => this.contracts.save({
|
|
79
|
+
path,
|
|
80
|
+
name,
|
|
81
|
+
version,
|
|
82
|
+
source,
|
|
83
|
+
parameters: declaration.parameters.map(parameter => ({ name: parameter.name, type: typeToString(parameter.type) })),
|
|
84
|
+
returnType: typeToString(declaration.returnType),
|
|
85
|
+
ownerOrganizationId,
|
|
86
|
+
description: description ?? null,
|
|
87
|
+
});
|
|
88
|
+
const saved = (feeMicroUsd) => {
|
|
89
|
+
this.announce?.();
|
|
90
|
+
return { ok: true, contract: { path, name, version, feeMicroUsd } };
|
|
91
|
+
};
|
|
92
|
+
const versionExists = () => refuse(409, `${path}.${name}@${version} already exists: contracts are immutable, so deploy a new version`);
|
|
93
|
+
const fee = this.config.contractDeployFeeMicroUsd ?? 0;
|
|
94
|
+
const billed = caller.kind === 'client' && caller.organizationId !== this.config.organizationId && fee > 0;
|
|
95
|
+
if (!billed) {
|
|
96
|
+
const result = await save();
|
|
97
|
+
return result.ok ? saved(0) : result.refused === 'version_exists' ? versionExists() : refuse(409, `${path}.${name} belongs to another organization`);
|
|
98
|
+
}
|
|
99
|
+
const organizationId = this.config.organizationId;
|
|
100
|
+
if (!organizationId) {
|
|
101
|
+
return refuse(503, 'This node isn\'t enrolled in an organization, so it can\'t charge the deploy fee');
|
|
102
|
+
}
|
|
103
|
+
const runId = randomUUID();
|
|
104
|
+
const authorization = await this.operatorClient.authorizeRun({ token: caller.token, runId, source });
|
|
105
|
+
if (!authorization.ok) {
|
|
106
|
+
return refuse(authorization.status, authorization.error);
|
|
107
|
+
}
|
|
108
|
+
const voucher = this.invoker.createPaymentVoucher(organizationId, fee, path, name, 'deploy', [{ name: 'version', value: version }], runId);
|
|
109
|
+
let refusal;
|
|
110
|
+
const result = await settlePaidCall(this.operatorClient, voucher, async () => {
|
|
111
|
+
const result = await save();
|
|
112
|
+
if (result.ok) {
|
|
113
|
+
return { value: true, error: null };
|
|
114
|
+
}
|
|
115
|
+
refusal = result.refused === 'version_exists' ? versionExists() : refuse(409, `${path}.${name} belongs to another organization`);
|
|
116
|
+
return { value: null, error: { message: refusal.error } };
|
|
117
|
+
}, this.log);
|
|
118
|
+
// The deploy ran no script, so the run is charged nothing beyond the fee's receipt.
|
|
119
|
+
settleRunLater(this.operatorClient, runId, 'capture', 0, this.log);
|
|
120
|
+
if ('refused' in result) {
|
|
121
|
+
return refuse(result.refused.status, result.refused.error);
|
|
122
|
+
}
|
|
123
|
+
return refusal ?? saved(fee);
|
|
124
|
+
}
|
|
125
|
+
/** Deletes a version, or every version, of a contract. Its owner or the node's admin may. */
|
|
126
|
+
async delete(path, name, version, caller) {
|
|
127
|
+
const owner = await this.contracts.owner(path, name);
|
|
128
|
+
if (owner === undefined) {
|
|
129
|
+
return refuse(404, `No contract ${path}.${name}`);
|
|
130
|
+
}
|
|
131
|
+
if (caller.kind === 'client' && owner !== caller.organizationId) {
|
|
132
|
+
return refuse(403, `${path}.${name} belongs to another organization`);
|
|
133
|
+
}
|
|
134
|
+
if (!await this.contracts.delete(path, name, version)) {
|
|
135
|
+
return refuse(404, `No contract ${path}.${name}@${version}`);
|
|
136
|
+
}
|
|
137
|
+
// State outlives a version, so a new one keeps it, but not the contract.
|
|
138
|
+
if (await this.contracts.owner(path, name) === undefined) {
|
|
139
|
+
await this.state.deleteContract(path, name);
|
|
140
|
+
}
|
|
141
|
+
this.announce?.();
|
|
142
|
+
return { ok: true };
|
|
143
|
+
}
|
|
144
|
+
/** The contract's state variables and their scopes, for its owner and the node's admin. */
|
|
145
|
+
async readState(path, name, caller) {
|
|
146
|
+
const owner = await this.contracts.owner(path, name);
|
|
147
|
+
if (owner === undefined) {
|
|
148
|
+
return refuse(404, `No contract ${path}.${name}`);
|
|
149
|
+
}
|
|
150
|
+
if (caller.kind === 'client' && owner !== caller.organizationId) {
|
|
151
|
+
return refuse(403, `${path}.${name} belongs to another organization`);
|
|
152
|
+
}
|
|
153
|
+
return { ok: true, variables: await this.state.list(path, name) };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Runs a contract for another node's call, paid by its voucher, which the route has checked. The operator
|
|
157
|
+
* holds the call's cost as a run named by the voucher's `invocationId`, unless the caller is in this
|
|
158
|
+
* node's organization. `depth` is how many contracts deep the call is. The contract sees the payer the
|
|
159
|
+
* operator authorized, and the voucher's key, as `caller`.
|
|
160
|
+
*/
|
|
161
|
+
async serve(voucher, contract, parameters, depth) {
|
|
162
|
+
const authorization = await this.operatorClient.authorizeContractRun({ voucher, source: contract.source });
|
|
163
|
+
if (!authorization.ok) {
|
|
164
|
+
return { refused: { status: authorization.status, error: authorization.error } };
|
|
165
|
+
}
|
|
166
|
+
const context = { jwt: null, depth, caller: { org: authorization.payerOrganizationId ?? null, node: voucher.publicKey } };
|
|
167
|
+
if (authorization.free) {
|
|
168
|
+
return this.run(contract, parameters, context);
|
|
169
|
+
}
|
|
170
|
+
return this.runBilled(voucher.invocationId, contract, parameters, context);
|
|
171
|
+
}
|
|
172
|
+
async invokeLocal(resource, parameters, context) {
|
|
173
|
+
const failed = (reason) => ({ value: null, error: { message: `Failed to invoke ${resource.name}.${CONTRACT_FUNCTION}: ${reason}` } });
|
|
174
|
+
const depth = (context.depth ?? 0) + 1;
|
|
175
|
+
if (depth > MAX_CONTRACT_DEPTH) {
|
|
176
|
+
return failed(`contracts may call each other at most ${MAX_CONTRACT_DEPTH} deep`);
|
|
177
|
+
}
|
|
178
|
+
const contract = await this.contracts.latest(resource.path, resource.name);
|
|
179
|
+
if (!contract) {
|
|
180
|
+
return failed('the contract was deleted');
|
|
181
|
+
}
|
|
182
|
+
// A script that doesn't say who it runs for is this node's own.
|
|
183
|
+
const caller = context.caller ?? { org: this.config.organizationId ?? null, node: this.config.publicKey };
|
|
184
|
+
const runContext = { jwt: context.jwt ?? null, depth, caller, contracts: context.contracts };
|
|
185
|
+
if (context.runId === undefined) {
|
|
186
|
+
return this.run(contract, parameters, runContext);
|
|
187
|
+
}
|
|
188
|
+
// A billed script pays for the contract as another node's script would, from its run's hold.
|
|
189
|
+
const organizationId = this.config.organizationId;
|
|
190
|
+
if (!organizationId) {
|
|
191
|
+
return failed('this node is not enrolled in an organization that can be paid');
|
|
192
|
+
}
|
|
193
|
+
const costLimit = this.config.contractCostLimitMicroUsd ?? this.config.costLimitMicroUsd;
|
|
194
|
+
const voucher = this.invoker.createContractVoucher(organizationId, this.config.priceMicroUsdPerMillionTokens ?? 0, costLimit, resource.path, resource.name, CONTRACT_FUNCTION, parameters, context.runId);
|
|
195
|
+
const authorization = await this.operatorClient.authorizeContractRun({ voucher, source: contract.source });
|
|
196
|
+
if (!authorization.ok) {
|
|
197
|
+
return failed(authorization.error);
|
|
198
|
+
}
|
|
199
|
+
return this.runBilled(voucher.invocationId, contract, parameters, runContext);
|
|
200
|
+
}
|
|
201
|
+
/** Runs a contract under a held run, then captures the tokens it executed, or releases the run if it couldn't run. */
|
|
202
|
+
async runBilled(runId, contract, parameters, context) {
|
|
203
|
+
const meter = new TokenMeter();
|
|
204
|
+
try {
|
|
205
|
+
const result = await this.run(contract, parameters, { ...context, runId }, meter);
|
|
206
|
+
settleRunLater(this.operatorClient, runId, 'capture', meter.tokens, this.log);
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
settleRunLater(this.operatorClient, runId, 'release', meter.tokens, this.log);
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Runs a contract's script. Its calls are made at `context.depth`, and billed to `context.runId` when set.
|
|
216
|
+
* A script that fails is reported as the call's error, as a failing function is.
|
|
217
|
+
*
|
|
218
|
+
* A contract with state variables runs one call at a time on this node, and saves its state only if the
|
|
219
|
+
* run succeeds. A call that re-enters a contract already running in its own chain skips the lock, and
|
|
220
|
+
* whichever of the two saves second finds the state changed and fails, so no update is lost.
|
|
221
|
+
*/
|
|
222
|
+
async run(contract, parameters, context, meter) {
|
|
223
|
+
const label = `${contract.path}.${contract.name}`;
|
|
224
|
+
const failed = (reason) => ({ value: null, error: { message: `Contract ${label}@${contract.version} failed: ${reason}` } });
|
|
225
|
+
const script = parseScript(contract.source);
|
|
226
|
+
const stateful = script.body.some(statement => statement.kind === 'varDecl' && statement.state !== undefined);
|
|
227
|
+
let release;
|
|
228
|
+
if (stateful && !context.contracts?.includes(label)) {
|
|
229
|
+
release = await this.lock(label);
|
|
230
|
+
if (!release) {
|
|
231
|
+
return failed(`it is busy: other calls held it for ${this.lockTimeoutMs / 1000} seconds`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const result = await interpretParsed(script, {
|
|
236
|
+
resourceResolver: this.resolver,
|
|
237
|
+
resourceInvoker: this.invoker.forScript({ ...context, contracts: [...context.contracts ?? [], label] }),
|
|
238
|
+
parameters: Object.fromEntries(parameters.map(parameter => [parameter.name, parameter.value])),
|
|
239
|
+
meter,
|
|
240
|
+
builtins: { jwt: context.jwt, caller: context.caller },
|
|
241
|
+
state: stateful ? { get: (variable, scope) => this.state.get(contract.path, contract.name, variable, scope) } : undefined,
|
|
242
|
+
});
|
|
243
|
+
if (result.state.length > 0) {
|
|
244
|
+
const maxBytes = this.config.maxContractStateBytes ?? DEFAULT_MAX_CONTRACT_STATE_BYTES;
|
|
245
|
+
const committed = await this.state.commit(contract.path, contract.name, result.state, maxBytes);
|
|
246
|
+
if (!committed.ok) {
|
|
247
|
+
return failed(committed.reason === 'conflict'
|
|
248
|
+
? 'its state changed during the call, so nothing was saved; try again'
|
|
249
|
+
: `its state would be ${committed.bytes} bytes, more than this node's ${maxBytes}, so nothing was saved`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { value: result.returnValue, error: null };
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error instanceof RequestScriptError) {
|
|
256
|
+
return failed(error.message);
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
release?.();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Waits for the contract's previous calls on this node, then holds it until the returned release is
|
|
266
|
+
* called. Answers undefined if the wait passes `lockTimeoutMs`, which breaks a cycle of calls between
|
|
267
|
+
* nodes that leads back here.
|
|
268
|
+
*/
|
|
269
|
+
async lock(label) {
|
|
270
|
+
const previous = this.locks.get(label) ?? Promise.resolve();
|
|
271
|
+
let release;
|
|
272
|
+
const held = new Promise(resolve => {
|
|
273
|
+
release = resolve;
|
|
274
|
+
});
|
|
275
|
+
const tail = previous.then(() => held);
|
|
276
|
+
this.locks.set(label, tail);
|
|
277
|
+
void tail.then(() => {
|
|
278
|
+
if (this.locks.get(label) === tail) {
|
|
279
|
+
this.locks.delete(label);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
let timer;
|
|
283
|
+
const acquired = await Promise.race([
|
|
284
|
+
previous.then(() => true),
|
|
285
|
+
new Promise(resolve => {
|
|
286
|
+
timer = setTimeout(() => resolve(false), this.lockTimeoutMs);
|
|
287
|
+
}),
|
|
288
|
+
]);
|
|
289
|
+
clearTimeout(timer);
|
|
290
|
+
if (!acquired) {
|
|
291
|
+
// Gives up its turn, so the calls behind it go on once the earlier ones finish.
|
|
292
|
+
release();
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
return release;
|
|
296
|
+
}
|
|
297
|
+
}
|