@ixo/oracle-runtime-workers 0.41.3 → 0.42.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/package.json +2 -2
- package/src/attachments/download.ts +1 -1
- package/src/core/runtime-context.ts +2 -2
- package/src/do/ambient.ts +2 -1
- package/src/plugin-api/types.ts +4 -1
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +1 -1
- package/src/plugins/ixo-chain/http.ts +8 -4
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +14 -1
- package/src/plugins/ixo-chain/queries.ts +26 -0
- package/src/plugins/pod-creator/chain-gateway.ts +123 -31
- package/src/plugins/pod-creator/config.ts +81 -1
- package/src/plugins/pod-creator/create-session-store.ts +159 -7
- package/src/plugins/pod-creator/create-tools.ts +403 -28
- package/src/plugins/pod-creator/domain-card.ts +67 -0
- package/src/plugins/pod-creator/index.ts +24 -2
- package/src/plugins/pod-creator/ixo-chain-gateway.ts +901 -0
- package/src/plugins/pod-creator/matrix-media.ts +486 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +5 -2
- package/src/plugins/pod-creator/pod-creator.plugin.ts +30 -9
- package/src/plugins/pod-creator/stage.ts +30 -0
- package/src/plugins/pod-creator/sub-agents.ts +204 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixo/oracle-runtime-workers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.0",
|
|
4
4
|
"description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -67,9 +67,9 @@
|
|
|
67
67
|
"protobufjs": "^7.6.5",
|
|
68
68
|
"@ixo/topic-protocol": "1.0.0-rc.8",
|
|
69
69
|
"survey-core": "2.3.11",
|
|
70
|
+
"@ixo/common": "1.8.0",
|
|
70
71
|
"@ixo/ixo-transaction": "0.2.0",
|
|
71
72
|
"@ixo/ucan": "2.3.0",
|
|
72
|
-
"@ixo/common": "1.8.0",
|
|
73
73
|
"@ixo/action-interactions": "0.1.1"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
@@ -168,7 +168,7 @@ export class HomeserverUnauthorizedError extends Error {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/** `mxc://<server>/<media id>`, the parts checked before they go into a URL. */
|
|
171
|
-
const MXC_PATTERN = /^mxc:\/\/([A-Za-z0-9.:[\]-]+)\/([A-Za-z0-9_-]+)$/;
|
|
171
|
+
export const MXC_PATTERN = /^mxc:\/\/([A-Za-z0-9.:[\]-]+)\/([A-Za-z0-9_-]+)$/;
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
174
|
* Open the media behind `mxc` on the homeserver as the account behind
|
|
@@ -95,7 +95,7 @@ export interface MatrixAdapter {
|
|
|
95
95
|
getRoomState(roomId: string): Promise<RoomStateSnapshot>;
|
|
96
96
|
getEventById(roomId: string, eventId: string): Promise<MatrixEvent>;
|
|
97
97
|
/** See `MatrixGatewayObject.botCredentials`. */
|
|
98
|
-
botCredentials(): Promise<BotCredentials>;
|
|
98
|
+
botCredentials(opts?: { rejected?: string }): Promise<BotCredentials>;
|
|
99
99
|
/** Group rooms: room kind and channel memory (see `plugin-api/types.ts`). Hosts without a gateway omit them. */
|
|
100
100
|
roomInfo?: RuntimeContext['matrix']['roomInfo'];
|
|
101
101
|
channelMemory?: RuntimeContext['matrix']['channelMemory'];
|
|
@@ -497,7 +497,7 @@ export function buildRuntimeContext<TConfig = MergedConfig>(
|
|
|
497
497
|
matrix: {
|
|
498
498
|
postToRoom: (roomId, content, opts) =>
|
|
499
499
|
ambient.matrix.postToRoom(roomId, content, opts),
|
|
500
|
-
botCredentials: () => ambient.matrix.botCredentials(),
|
|
500
|
+
botCredentials: (opts) => ambient.matrix.botCredentials(opts),
|
|
501
501
|
postEvent: (roomId, eventType, content, opts) =>
|
|
502
502
|
ambient.matrix.postEvent(roomId, eventType, content, opts),
|
|
503
503
|
getRoomState: (roomId) => ambient.matrix.getRoomState(roomId),
|
package/src/do/ambient.ts
CHANGED
|
@@ -183,7 +183,8 @@ export function createMatrixAdapter(
|
|
|
183
183
|
),
|
|
184
184
|
postEvent: (roomId, eventType, content, opts) =>
|
|
185
185
|
gateway.sendEvent(roomId, eventType, JSON.stringify(content), opts ?? {}),
|
|
186
|
-
botCredentials: () =>
|
|
186
|
+
botCredentials: (opts?: { rejected?: string }) =>
|
|
187
|
+
gateway.botCredentials(opts?.rejected ? { rejected: opts.rejected } : {}),
|
|
187
188
|
roomInfo: (roomId) => gateway.groupChatRoomInfo(roomId),
|
|
188
189
|
channelMemory: {
|
|
189
190
|
recall: async (roomId, limit) =>
|
package/src/plugin-api/types.ts
CHANGED
|
@@ -818,8 +818,11 @@ export interface RuntimeContext<TConfig = MergedConfig> {
|
|
|
818
818
|
/**
|
|
819
819
|
* Credentials of the oracle's own crypto-less device for plugins that run
|
|
820
820
|
* their own Matrix client (editor, flows). Minted and kept by the gateway.
|
|
821
|
+
* A caller whose request the homeserver answered 401 passes the token it
|
|
822
|
+
* used as `rejected`: the host then re-validates the device (or logs it
|
|
823
|
+
* in again) and answers with fresh credentials, so one retry can succeed.
|
|
821
824
|
*/
|
|
822
|
-
botCredentials: () => Promise<{
|
|
825
|
+
botCredentials: (opts?: { rejected?: string }) => Promise<{
|
|
823
826
|
baseUrl: string;
|
|
824
827
|
userId: string;
|
|
825
828
|
accessToken: string;
|
|
@@ -14,7 +14,7 @@ import { createDomainIndexerTools } from './domain-indexer-tools';
|
|
|
14
14
|
* the core `NETWORK` env var. Operators can override with the optional
|
|
15
15
|
* `DOMAIN_INDEXER_URL` env var (useful for local/staging deployments).
|
|
16
16
|
*/
|
|
17
|
-
const DOMAIN_INDEXER_URLS = {
|
|
17
|
+
export const DOMAIN_INDEXER_URLS = {
|
|
18
18
|
mainnet: 'https://domain-indexer.ixo.earth',
|
|
19
19
|
testnet: 'https://domain-indexer.testnet.ixo.earth',
|
|
20
20
|
devnet: 'https://domain-indexer.devnet.ixo.earth',
|
|
@@ -151,6 +151,8 @@ export interface ChainCall {
|
|
|
151
151
|
deadline: number;
|
|
152
152
|
signal: AbortSignal;
|
|
153
153
|
fetch: typeof fetch;
|
|
154
|
+
/** How the call waits before a retry; {@link sleep} unless a test injects its own clock. */
|
|
155
|
+
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
/**
|
|
@@ -192,7 +194,7 @@ export type Outcome<T> =
|
|
|
192
194
|
| { ok: false; failure: Failure };
|
|
193
195
|
|
|
194
196
|
/** The turn's abort reason as an error (a non-error reason becomes an `AbortError`). */
|
|
195
|
-
function abortReason(signal: AbortSignal): Error {
|
|
197
|
+
export function abortReason(signal: AbortSignal): Error {
|
|
196
198
|
return signal.reason instanceof Error
|
|
197
199
|
? signal.reason
|
|
198
200
|
: new DOMException('Aborted', 'AbortError');
|
|
@@ -288,7 +290,7 @@ export class ReadsInFlight<T> {
|
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
/** Resolves after `ms`; rejects with the turn's abort reason if it ends first. */
|
|
291
|
-
function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
293
|
+
export function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
292
294
|
return new Promise((resolve, reject) => {
|
|
293
295
|
if (signal.aborted) {
|
|
294
296
|
reject(abortReason(signal));
|
|
@@ -394,7 +396,7 @@ export async function withRetry<T>(
|
|
|
394
396
|
: { ok: false, failure: first.failure };
|
|
395
397
|
if (!first.retry || Date.now() + wait + MIN_ATTEMPT_MS > call.deadline)
|
|
396
398
|
return giveUp();
|
|
397
|
-
await sleep(wait, call.signal);
|
|
399
|
+
await (call.sleep ?? sleep)(wait, call.signal);
|
|
398
400
|
const second = await attempt();
|
|
399
401
|
recordHealth(read.upstream, second);
|
|
400
402
|
if (second.ok) {
|
|
@@ -518,7 +520,9 @@ const outOfBudget = (note: string): Attempt<never> => ({
|
|
|
518
520
|
* The body of an error answer as text, or undefined when it is longer than
|
|
519
521
|
* {@link ERROR_BODY_MAX_BYTES} (the rest is not read).
|
|
520
522
|
*/
|
|
521
|
-
async function errorBodyOf(
|
|
523
|
+
export async function errorBodyOf(
|
|
524
|
+
response: Response,
|
|
525
|
+
): Promise<string | undefined> {
|
|
522
526
|
const reader = response.body?.getReader();
|
|
523
527
|
if (!reader) return '';
|
|
524
528
|
const decoder = new TextDecoder();
|
|
@@ -79,12 +79,25 @@ function networkOf(config: MergedConfig): Network {
|
|
|
79
79
|
|
|
80
80
|
/** The REST base URL: `CHAIN_REST_URL` when set, else the network's default. */
|
|
81
81
|
export function chainRestUrl(config: MergedConfig): string {
|
|
82
|
+
return chainRestUrlFor(networkOf(config), config);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The REST base URL for `network`: `CHAIN_REST_URL` when set (the
|
|
87
|
+
* operator's node for the oracle's network), else that network's default.
|
|
88
|
+
* For a caller that already knows its network rather than reading
|
|
89
|
+
* `NETWORK` itself.
|
|
90
|
+
*/
|
|
91
|
+
export function chainRestUrlFor(
|
|
92
|
+
network: Network,
|
|
93
|
+
config: MergedConfig,
|
|
94
|
+
): string {
|
|
82
95
|
const override = configSchema.shape.CHAIN_REST_URL.safeParse(
|
|
83
96
|
config.CHAIN_REST_URL,
|
|
84
97
|
);
|
|
85
98
|
return override.success && override.data
|
|
86
99
|
? override.data
|
|
87
|
-
: CHAIN_REST_URLS[
|
|
100
|
+
: CHAIN_REST_URLS[network];
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
/** The names worker's origin: `IXO_NAMES_URL` when set, else the network's default. */
|
|
@@ -221,6 +221,32 @@ export function isBech32(address: string, prefix: string): boolean {
|
|
|
221
221
|
return padding < 5 && (last & ((1 << padding) - 1)) === 0;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/**
|
|
225
|
+
* The bytes a valid bech32 `address` with the prefix `prefix` carries (for
|
|
226
|
+
* an account, the 20-byte hash of its public key); null when
|
|
227
|
+
* {@link isBech32} refuses it.
|
|
228
|
+
*/
|
|
229
|
+
export function bech32Bytes(
|
|
230
|
+
address: string,
|
|
231
|
+
prefix: string,
|
|
232
|
+
): Uint8Array | null {
|
|
233
|
+
if (!isBech32(address, prefix)) return null;
|
|
234
|
+
const data = address.slice(prefix.length + 1, -BECH32_CHECKSUM);
|
|
235
|
+
const bytes: number[] = [];
|
|
236
|
+
let buffer = 0;
|
|
237
|
+
let bits = 0;
|
|
238
|
+
for (const char of data) {
|
|
239
|
+
buffer = (buffer << 5) | BECH32_CHARSET.indexOf(char);
|
|
240
|
+
bits += 5;
|
|
241
|
+
if (bits >= 8) {
|
|
242
|
+
bits -= 8;
|
|
243
|
+
bytes.push((buffer >>> bits) & 0xff);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// The leftover bits are the zero padding isBech32 checked.
|
|
247
|
+
return Uint8Array.from(bytes);
|
|
248
|
+
}
|
|
249
|
+
|
|
224
250
|
/**
|
|
225
251
|
* The account an account field names: the user's own address when the
|
|
226
252
|
* field is omitted, else an `ixo1…` address or the address a
|
|
@@ -9,24 +9,87 @@ import type { ServicePodBlueprint } from './blueprint-types';
|
|
|
9
9
|
*/
|
|
10
10
|
export interface PreparedPodBatch {
|
|
11
11
|
/**
|
|
12
|
-
* The messages, in execution order: `MsgCreateEntity` first, then
|
|
13
|
-
*
|
|
14
|
-
* Each is `{ typeUrl, value }` in proto-JSON (camelCase fields,
|
|
15
|
-
* amounts as strings, bytes as base64
|
|
12
|
+
* The messages, in execution order: `MsgCreateEntity` first, then only
|
|
13
|
+
* `POD_BATCH_TYPE_URLS` (the bundled gateway sends `MsgCreateEntity`
|
|
14
|
+
* alone). Each is `{ typeUrl, value }` in proto-JSON (camelCase fields,
|
|
15
|
+
* integer amounts as strings, bytes as base64, timestamps as RFC 3339
|
|
16
|
+
* strings) with exactly its catalog fields.
|
|
16
17
|
*/
|
|
17
18
|
messages: ITrxMsg[];
|
|
18
19
|
/** Human-readable summary of what the batch creates (at most 1000 characters). */
|
|
19
20
|
summary: string;
|
|
20
21
|
/** Estimated cost, when the gateway can compute it. */
|
|
21
22
|
estimatedCost?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Public documents the messages name (by URL and SHA-256 proof) that are
|
|
25
|
+
* not published yet: `publishDocuments` publishes them once the user
|
|
26
|
+
* approved, before the wallet is asked to sign. Empty when every document
|
|
27
|
+
* the messages name is already public.
|
|
28
|
+
*/
|
|
29
|
+
documents: PendingDocument[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A public document a batch names (the POD's profile, its domain card),
|
|
34
|
+
* reserved on the oracle's homeserver when the batch is prepared and
|
|
35
|
+
* uploaded only after the user approved it.
|
|
36
|
+
*/
|
|
37
|
+
export interface PendingDocument {
|
|
38
|
+
/** The reserved media it is uploaded as: `mxc://<server>/<media id>`. */
|
|
39
|
+
mxc: string;
|
|
40
|
+
/** Its media type, e.g. `application/ld+json`. */
|
|
41
|
+
contentType: string;
|
|
42
|
+
/** The file name the homeserver records. */
|
|
43
|
+
filename: string;
|
|
44
|
+
/** The exact text: the message carries the SHA-256 of these bytes as its proof. */
|
|
45
|
+
text: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* An expected refusal (a missing identity, a homeserver that is down), which
|
|
50
|
+
* the tools report to the model as a result instead of throwing.
|
|
51
|
+
*/
|
|
52
|
+
export interface GatewayRefusal {
|
|
53
|
+
ok: false;
|
|
54
|
+
message: string;
|
|
55
|
+
/** Whether the same call can succeed later unchanged (something was down). */
|
|
56
|
+
retryable: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A gateway answer: the value, or why there is none. */
|
|
60
|
+
export type GatewayResult<T extends object> =
|
|
61
|
+
| ({ ok: true } & T)
|
|
62
|
+
| GatewayRefusal;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What a broadcast transaction says about the POD. `pending`: not on chain
|
|
66
|
+
* (or not indexed) within the lookup's time, ask again later with the same
|
|
67
|
+
* hash; `failed`: it failed, or it is not the POD this conversation sent, so
|
|
68
|
+
* asking again cannot help; `unavailable`: the chain could not be read.
|
|
69
|
+
*/
|
|
70
|
+
export type PodConfirmation =
|
|
71
|
+
| { status: 'created'; podDid: string; summary: string }
|
|
72
|
+
| {
|
|
73
|
+
status: 'pending' | 'failed' | 'unavailable';
|
|
74
|
+
message: string;
|
|
75
|
+
retryable: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** What the user's wallet was asked to sign, as the create session records it. */
|
|
79
|
+
export interface SentPodBatch {
|
|
80
|
+
/** The messages exactly as sent (proto-JSON, catalog-validated). */
|
|
81
|
+
messages: ITrxMsg[];
|
|
82
|
+
/** The profile the POD was prepared with (for its domain card). */
|
|
83
|
+
profile: PodProfile;
|
|
22
84
|
}
|
|
23
85
|
|
|
24
86
|
/**
|
|
25
87
|
* The message types a POD creation batch may carry. The chain runs the
|
|
26
88
|
* messages of one transaction in order, all or nothing, so a later message
|
|
27
89
|
* can name what an earlier one creates — the entity DID, its admin account,
|
|
28
|
-
* the collection id —
|
|
29
|
-
*
|
|
90
|
+
* the collection id — only by predicting those values (otherwise the whole
|
|
91
|
+
* transaction fails and nothing is created). The bundled gateway does not
|
|
92
|
+
* predict: it sends `MsgCreateEntity` alone.
|
|
30
93
|
*/
|
|
31
94
|
export const POD_BATCH_TYPE_URLS = [
|
|
32
95
|
'/ixo.entity.v1beta1.MsgCreateEntity',
|
|
@@ -62,49 +125,78 @@ export function podBatchProblem(
|
|
|
62
125
|
return null;
|
|
63
126
|
}
|
|
64
127
|
|
|
65
|
-
/**
|
|
66
|
-
export interface
|
|
67
|
-
|
|
68
|
-
|
|
128
|
+
/** What the user agreed to call the POD, for its on-chain profile. */
|
|
129
|
+
export interface PodProfile {
|
|
130
|
+
/** The POD's name: plain text, 1 to 100 characters. */
|
|
131
|
+
name: string;
|
|
132
|
+
/** What the POD does, in plain text (at most 600 characters). */
|
|
133
|
+
description?: string;
|
|
69
134
|
}
|
|
70
135
|
|
|
71
136
|
/**
|
|
72
|
-
* The chain-facing seam for the create path. The
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* the plugin validates against the `@ixo/ixo-transaction` catalog before
|
|
78
|
-
* storing them, and resolves results.
|
|
137
|
+
* The chain-facing seam for the create path. The oracle never signs
|
|
138
|
+
* creation — the user's wallet does — so a gateway only ever returns
|
|
139
|
+
* proto-JSON messages, which the plugin validates against the
|
|
140
|
+
* `@ixo/ixo-transaction` catalog before storing them, and resolves the
|
|
141
|
+
* broadcast transaction afterwards.
|
|
79
142
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
143
|
+
* The bundled implementation is `createIxoChainGateway()`
|
|
144
|
+
* (`ixo-chain-gateway.ts`): one `MsgCreateEntity` owned by the user, its
|
|
145
|
+
* profile reserved on the oracle's homeserver and published after approval,
|
|
146
|
+
* read back from chain REST; then the POD's domain card. Every method
|
|
147
|
+
* receives the request `RuntimeContext` (the user, the config, the turn's
|
|
148
|
+
* abort signal, the oracle's Matrix device) and answers an expected refusal
|
|
149
|
+
* as a result; a throw is a programming error. The seam is injected so the
|
|
150
|
+
* create path stays unit-testable without a live homeserver or chain, and
|
|
151
|
+
* so a fork can compose a different batch.
|
|
86
152
|
*/
|
|
87
153
|
export interface ChainGateway {
|
|
88
154
|
preparePodBatch(
|
|
89
|
-
input: {
|
|
155
|
+
input: {
|
|
156
|
+
blueprint: ServicePodBlueprint;
|
|
157
|
+
network: Network;
|
|
158
|
+
profile: PodProfile;
|
|
159
|
+
},
|
|
90
160
|
ctx: RuntimeContext,
|
|
91
|
-
): Promise<PreparedPodBatch
|
|
161
|
+
): Promise<GatewayResult<PreparedPodBatch>>;
|
|
162
|
+
/** Publish a prepared batch's pending documents; publishing one twice is a success. */
|
|
163
|
+
publishDocuments(
|
|
164
|
+
input: { documents: readonly PendingDocument[] },
|
|
165
|
+
ctx: RuntimeContext,
|
|
166
|
+
): Promise<GatewayResult<object>>;
|
|
167
|
+
/** Read the transaction back and check it is the POD `sent` describes. */
|
|
92
168
|
confirmPodCreation(
|
|
93
|
-
input: { txHash: string; network: Network },
|
|
169
|
+
input: { txHash: string; network: Network; sent: SentPodBatch },
|
|
170
|
+
ctx: RuntimeContext,
|
|
171
|
+
): Promise<PodConfirmation>;
|
|
172
|
+
/** The batch that adds a confirmed POD's public domain card (one message). */
|
|
173
|
+
prepareDomainCard(
|
|
174
|
+
input: { network: Network; podDid: string; profile: PodProfile },
|
|
175
|
+
ctx: RuntimeContext,
|
|
176
|
+
): Promise<GatewayResult<PreparedPodBatch>>;
|
|
177
|
+
/** Ask the domain indexer to index the POD now; best effort, never throws. */
|
|
178
|
+
requestIndexing(
|
|
179
|
+
input: { network: Network; podDid: string },
|
|
94
180
|
ctx: RuntimeContext,
|
|
95
|
-
): Promise<
|
|
181
|
+
): Promise<void>;
|
|
96
182
|
}
|
|
97
183
|
|
|
98
184
|
const notConfigured = (): never => {
|
|
99
185
|
throw new Error(
|
|
100
|
-
'ChainGateway not configured:
|
|
101
|
-
'
|
|
102
|
-
'transactions.',
|
|
186
|
+
'ChainGateway not configured: this oracle was built without a chain ' +
|
|
187
|
+
'gateway, so it cannot prepare or confirm POD creation transactions.',
|
|
103
188
|
);
|
|
104
189
|
};
|
|
105
190
|
|
|
106
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* A gateway that refuses every call, for a fork that wants the design
|
|
193
|
+
* pipeline without on-chain creation: the create tools report creation as
|
|
194
|
+
* unavailable without calling it.
|
|
195
|
+
*/
|
|
107
196
|
export const notConfiguredChainGateway: ChainGateway = {
|
|
108
197
|
preparePodBatch: async () => notConfigured(),
|
|
198
|
+
publishDocuments: async () => notConfigured(),
|
|
109
199
|
confirmPodCreation: async () => notConfigured(),
|
|
200
|
+
prepareDomainCard: async () => notConfigured(),
|
|
201
|
+
requestIndexing: async () => notConfigured(),
|
|
110
202
|
};
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '@ixo/ixo-transaction';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import type { RuntimeContext } from '../../plugin-api/types';
|
|
8
|
+
import { DOMAIN_INDEXER_URLS } from '../domain-indexer/domain-indexer.plugin';
|
|
8
9
|
import {
|
|
9
10
|
chainIdConfigSchema,
|
|
10
11
|
chainIdsFromConfig,
|
|
@@ -18,6 +19,9 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
export const DEFAULT_NETWORK: Network = 'testnet';
|
|
20
21
|
|
|
22
|
+
/** An IXO entity DID as the chain derives it. */
|
|
23
|
+
const ENTITY_DID = /^did:ixo:entity:[0-9a-f]{32}$/;
|
|
24
|
+
|
|
21
25
|
/**
|
|
22
26
|
* Plugin-owned env vars. The capsules registry URL and `NETWORK` are read as
|
|
23
27
|
* siblings from the merged config (owned by the skills plugin / base schema),
|
|
@@ -42,14 +46,49 @@ export const podCreatorConfigSchema = z.object({
|
|
|
42
46
|
.default(false),
|
|
43
47
|
/** The chain id each network's signing request names. */
|
|
44
48
|
...chainIdConfigSchema.shape,
|
|
49
|
+
/**
|
|
50
|
+
* The relayer node a created POD names, instead of the network's default
|
|
51
|
+
* ({@link POD_RELAYER_NODE_DIDS}).
|
|
52
|
+
*/
|
|
53
|
+
POD_CREATOR_RELAYER_NODE_DID: z
|
|
54
|
+
.string()
|
|
55
|
+
.regex(ENTITY_DID, 'expected an entity DID: did:ixo:entity:<32 hex>')
|
|
56
|
+
.optional(),
|
|
57
|
+
/**
|
|
58
|
+
* The Portal a created POD is linked to, instead of the network's default
|
|
59
|
+
* ({@link PORTAL_URLS}).
|
|
60
|
+
*/
|
|
61
|
+
POD_CREATOR_PORTAL_URL: z.url().optional(),
|
|
45
62
|
});
|
|
46
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The relayer node each network's Portal names on the domains (and PODs) it
|
|
66
|
+
* creates: its `NEXT_PUBLIC_DAO_DID` (ixo-portal `lib/entity/createEntity.ts`),
|
|
67
|
+
* as read from the Portal's recent "Create domain:" transactions. The
|
|
68
|
+
* mainnet Portal lists only entities of this relayer that are verified.
|
|
69
|
+
* Not the oracles' relayer (`lib/oracle/networkConfig.ts`), which only
|
|
70
|
+
* oracle entities name.
|
|
71
|
+
*/
|
|
72
|
+
export const POD_RELAYER_NODE_DIDS: Readonly<Record<Network, string>> = {
|
|
73
|
+
mainnet: 'did:ixo:entity:47d9c64891e77cb8e588d33b83ab3042',
|
|
74
|
+
testnet: 'did:ixo:entity:3d079ebc0b332aad3305bb4a51c72edb',
|
|
75
|
+
devnet: 'did:ixo:entity:ac12c6f8f299071eb24f2a10591d7ab7',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The Portal each network's PODs are opened in. */
|
|
79
|
+
export const PORTAL_URLS: Readonly<Record<Network, string>> = {
|
|
80
|
+
mainnet: 'https://portal.qi.space',
|
|
81
|
+
testnet: 'https://test.portal.qi.space',
|
|
82
|
+
devnet: 'https://dev.portal.qi.space',
|
|
83
|
+
};
|
|
84
|
+
|
|
47
85
|
/** Sibling env + plugin flags the tools read at request time. */
|
|
48
86
|
const configReadSchema = z.object({
|
|
49
87
|
NETWORK: NetworkSchema.optional(),
|
|
50
88
|
POD_CREATOR_ALLOW_MAINNET:
|
|
51
89
|
podCreatorConfigSchema.shape.POD_CREATOR_ALLOW_MAINNET.optional(),
|
|
52
90
|
...chainIdConfigSchema.shape,
|
|
91
|
+
POD_CREATOR_PORTAL_URL: podCreatorConfigSchema.shape.POD_CREATOR_PORTAL_URL,
|
|
53
92
|
});
|
|
54
93
|
|
|
55
94
|
export interface PodCreatorConfig {
|
|
@@ -57,8 +96,12 @@ export interface PodCreatorConfig {
|
|
|
57
96
|
mainnetAllowed: boolean;
|
|
58
97
|
/** The chain id each network's signing request names. */
|
|
59
98
|
chainIds: Readonly<Record<Network, string>>;
|
|
99
|
+
/** The Portal of `network`, without a trailing slash. */
|
|
100
|
+
portalUrl: string;
|
|
60
101
|
}
|
|
61
102
|
|
|
103
|
+
const withoutSlash = (url: string): string => url.replace(/\/+$/, '');
|
|
104
|
+
|
|
62
105
|
/**
|
|
63
106
|
* Network routing + the mainnet opt-in, read from the merged config. Falls
|
|
64
107
|
* back to testnet with mainnet disabled when the config is unreadable — and
|
|
@@ -75,11 +118,48 @@ export function readPodCreatorConfig(ctx: RuntimeContext): PodCreatorConfig {
|
|
|
75
118
|
network: DEFAULT_NETWORK,
|
|
76
119
|
mainnetAllowed: false,
|
|
77
120
|
chainIds: DEFAULT_CHAIN_IDS,
|
|
121
|
+
portalUrl: PORTAL_URLS[DEFAULT_NETWORK],
|
|
78
122
|
};
|
|
79
123
|
}
|
|
124
|
+
const network = parsed.data.NETWORK ?? DEFAULT_NETWORK;
|
|
80
125
|
return {
|
|
81
|
-
network
|
|
126
|
+
network,
|
|
82
127
|
mainnetAllowed: parsed.data.POD_CREATOR_ALLOW_MAINNET ?? false,
|
|
83
128
|
chainIds: chainIdsFromConfig(parsed.data),
|
|
129
|
+
portalUrl: withoutSlash(
|
|
130
|
+
parsed.data.POD_CREATOR_PORTAL_URL ?? PORTAL_URLS[network],
|
|
131
|
+
),
|
|
84
132
|
};
|
|
85
133
|
}
|
|
134
|
+
|
|
135
|
+
const relayerReadSchema = z.object({
|
|
136
|
+
POD_CREATOR_RELAYER_NODE_DID:
|
|
137
|
+
podCreatorConfigSchema.shape.POD_CREATOR_RELAYER_NODE_DID,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/** The relayer node a POD created on `network` names: the operator's, else the network's Portal's. */
|
|
141
|
+
export function relayerNodeDidFor(
|
|
142
|
+
ctx: RuntimeContext,
|
|
143
|
+
network: Network,
|
|
144
|
+
): string {
|
|
145
|
+
const parsed = relayerReadSchema.safeParse(ctx.config);
|
|
146
|
+
return (
|
|
147
|
+
(parsed.success ? parsed.data.POD_CREATOR_RELAYER_NODE_DID : undefined) ??
|
|
148
|
+
POD_RELAYER_NODE_DIDS[network]
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `DOMAIN_INDEXER_URL` is the domain-indexer plugin's; read here as a sibling. */
|
|
153
|
+
const indexerReadSchema = z.object({ DOMAIN_INDEXER_URL: z.url().optional() });
|
|
154
|
+
|
|
155
|
+
/** The domain indexer of `network`, without a trailing slash. */
|
|
156
|
+
export function domainIndexerUrlFor(
|
|
157
|
+
ctx: RuntimeContext,
|
|
158
|
+
network: Network,
|
|
159
|
+
): string {
|
|
160
|
+
const parsed = indexerReadSchema.safeParse(ctx.config);
|
|
161
|
+
return withoutSlash(
|
|
162
|
+
(parsed.success ? parsed.data.DOMAIN_INDEXER_URL : undefined) ??
|
|
163
|
+
DOMAIN_INDEXER_URLS[network],
|
|
164
|
+
);
|
|
165
|
+
}
|