@ixo/oracle-runtime-workers 0.29.1 → 0.31.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.
Files changed (74) hide show
  1. package/package.json +7 -5
  2. package/src/attachments/channel-links.ts +135 -0
  3. package/src/attachments/channel.ts +203 -0
  4. package/src/attachments/download.ts +23 -6
  5. package/src/attachments/index.ts +23 -0
  6. package/src/attachments/pipeline.ts +120 -42
  7. package/src/attachments/retention.ts +11 -3
  8. package/src/attachments/sanitize.ts +20 -8
  9. package/src/attachments/types.ts +7 -0
  10. package/src/attachments/view.ts +24 -7
  11. package/src/channels/contract.ts +69 -0
  12. package/src/channels/turns.ts +23 -1
  13. package/src/core/main-agent-types.ts +2 -0
  14. package/src/core/runtime-context.ts +3 -0
  15. package/src/delivery/plan.ts +3 -1
  16. package/src/do/contracts.ts +3 -0
  17. package/src/do/turn-attachments.ts +103 -0
  18. package/src/do/user-oracle-do.ts +142 -42
  19. package/src/hub-actions/client.ts +87 -7
  20. package/src/hub-actions/hub-text.ts +82 -0
  21. package/src/hub-actions/request.ts +7 -4
  22. package/src/hub-actions/store.ts +5 -1
  23. package/src/hub-actions/transfer.ts +23 -6
  24. package/src/index.ts +1 -0
  25. package/src/plugin-api/index.ts +1 -0
  26. package/src/plugin-api/types.ts +43 -2
  27. package/src/plugins/attachments/view-attachment-tool.ts +7 -3
  28. package/src/plugins/hub-actions/hub-actions.plugin.ts +96 -17
  29. package/src/plugins/ixo-chain/balances.ts +141 -0
  30. package/src/plugins/ixo-chain/blocksync.ts +17 -8
  31. package/src/plugins/ixo-chain/denoms.ts +84 -36
  32. package/src/plugins/ixo-chain/http.ts +315 -70
  33. package/src/plugins/ixo-chain/ixo-chain.plugin.ts +182 -301
  34. package/src/plugins/ixo-chain/kinds/chain/account.ts +516 -0
  35. package/src/plugins/ixo-chain/kinds/chain/claims.ts +287 -0
  36. package/src/plugins/ixo-chain/kinds/chain/economics.ts +237 -0
  37. package/src/plugins/ixo-chain/kinds/chain/governance.ts +455 -0
  38. package/src/plugins/ixo-chain/kinds/chain/grants.ts +682 -0
  39. package/src/plugins/ixo-chain/kinds/chain/index.ts +23 -0
  40. package/src/plugins/ixo-chain/kinds/chain/liquid-staking.ts +279 -0
  41. package/src/plugins/ixo-chain/kinds/chain/shared.ts +286 -0
  42. package/src/plugins/ixo-chain/kinds/chain/staking.ts +220 -0
  43. package/src/plugins/ixo-chain/kinds/coins.ts +112 -0
  44. package/src/plugins/ixo-chain/kinds/core.ts +246 -0
  45. package/src/plugins/ixo-chain/kinds/indexed/agent-deposits.ts +124 -0
  46. package/src/plugins/ixo-chain/kinds/indexed/dao-proposals.ts +379 -0
  47. package/src/plugins/ixo-chain/kinds/indexed/daos.ts +319 -0
  48. package/src/plugins/ixo-chain/kinds/indexed/disputes.ts +159 -0
  49. package/src/plugins/ixo-chain/kinds/indexed/evaluations.ts +172 -0
  50. package/src/plugins/ixo-chain/kinds/indexed/index.ts +21 -0
  51. package/src/plugins/ixo-chain/kinds/indexed/liquid-stake-history.ts +160 -0
  52. package/src/plugins/ixo-chain/kinds/indexed/member-budgets.ts +175 -0
  53. package/src/plugins/ixo-chain/kinds/indexed/shared.ts +211 -0
  54. package/src/plugins/ixo-chain/kinds/indexed/token-history.ts +205 -0
  55. package/src/plugins/ixo-chain/kinds/registry.ts +43 -0
  56. package/src/plugins/ixo-chain/kinds/types.ts +156 -0
  57. package/src/plugins/ixo-chain/lookup.ts +470 -0
  58. package/src/plugins/ixo-chain/overview.ts +1060 -0
  59. package/src/plugins/ixo-chain/queries.ts +170 -138
  60. package/src/plugins/ixo-chain/rest.ts +135 -54
  61. package/src/plugins/ixo-chain/summaries.ts +3 -2
  62. package/src/plugins/yoma/catalog.ts +780 -0
  63. package/src/plugins/yoma/clef.ts +98 -0
  64. package/src/plugins/yoma/drafts.ts +339 -0
  65. package/src/plugins/yoma/engine.ts +926 -0
  66. package/src/plugins/yoma/index.ts +13 -0
  67. package/src/plugins/yoma/judge.ts +358 -0
  68. package/src/plugins/yoma/permission.ts +62 -0
  69. package/src/plugins/yoma/reads.ts +195 -0
  70. package/src/plugins/yoma/submission.ts +534 -0
  71. package/src/plugins/yoma/text.ts +16 -0
  72. package/src/plugins/yoma/work.ts +190 -0
  73. package/src/plugins/yoma/yoma.plugin.ts +2162 -0
  74. package/src/sqlite/serialization.ts +2 -0
@@ -1,5 +1,6 @@
1
1
  /**
2
- * The `chain_lookup` requests and the one Blocksync document they become.
2
+ * The core `chain_lookup` kinds' arguments and the Blocksync root field
3
+ * each request becomes.
3
4
  *
4
5
  * Every field name, argument and enum below is checked against the
5
6
  * ixo-blocksync-api schema (`public/graphql/schema.graphql`; see
@@ -15,13 +16,11 @@
15
16
  import { IxoAddressSchema } from '@ixo/ixo-transaction';
16
17
  import { z } from 'zod';
17
18
  import { ixoAddressOf, type UserIdentity } from '../../core/user-identity';
18
- import type { AliasedField } from './blocksync';
19
19
 
20
- const MAX_REQUESTS = 8;
21
20
  const MAX_FIRST = 20;
22
21
  const DEFAULT_FIRST = 10;
23
22
  /**
24
- * Rows a document of several requests reads in all: each list request is
23
+ * Rows a call of several requests reads in all: each list request is
25
24
  * capped at `max(5, floor(40 / requests))`.
26
25
  */
27
26
  const SHARED_ROWS = 40;
@@ -40,99 +39,106 @@ const TX_HASH = /^[A-F0-9]{64}$/i;
40
39
  const CURSOR = /^[A-Za-z0-9+/=_-]{1,200}$/;
41
40
  const ENTITY_TYPE = /^[A-Za-z0-9/_.:-]{1,64}$/;
42
41
 
43
- // The account-field format and `first`'s meaning are stated once, in the
44
- // tool description; each field here only names its role. Lengths are not
45
- // capped here: every string is checked against its kind's bounded pattern
46
- // before it reaches the document.
47
- const first = z.number().int().min(1).max(MAX_FIRST).optional();
48
- const account = (role: string) => z.string().optional().describe(role);
42
+ // These schemas are shown only through `describe_chain_kinds`, so each
43
+ // field says its format in full. Lengths are not capped here: every string
44
+ // is checked against its kind's bounded pattern before it reaches the
45
+ // document.
46
+ /** A list kind's `first` argument. */
47
+ export const firstArg = z
48
+ .number()
49
+ .int()
50
+ .min(1)
51
+ .max(MAX_FIRST)
52
+ .optional()
53
+ .describe(
54
+ 'Rows to read, 1–20 (default 10; fewer when a call has several requests).',
55
+ );
56
+ /** An account argument (`role` names whose it is), the user's by default. */
57
+ export const accountArg = (role: string) =>
58
+ z
59
+ .string()
60
+ .optional()
61
+ .describe(
62
+ `${role}: an ixo1… address or did:ixo:ixo1… DID. Default: the user.`,
63
+ );
49
64
 
50
- export const chainRequestSchema = z.discriminatedUnion('kind', [
51
- z.strictObject({
52
- kind: z.literal('claims'),
53
- agent: account('Claim submitter.'),
54
- collectionId: z.string().optional(),
55
- first,
56
- after: z.string().optional().describe('Previous page endCursor.'),
65
+ /** Each core kind's arguments (strict: an unknown key refuses the request). */
66
+ export const CORE_ARGS = {
67
+ claims: z.strictObject({
68
+ agent: accountArg('Claim submitter'),
69
+ collectionId: z
70
+ .string()
71
+ .optional()
72
+ .describe('Only claims in this collection, e.g. "8250".'),
73
+ first: firstArg,
74
+ after: z
75
+ .string()
76
+ .optional()
77
+ .describe("The previous page's `endCursor`, for the next page."),
57
78
  }),
58
- z.strictObject({
59
- kind: z.literal('claim'),
79
+ claim: z.strictObject({
60
80
  claimId: z.string().describe('The claim id (a CID, e.g. bafkrei…).'),
61
81
  }),
62
- z.strictObject({
63
- kind: z.literal('claim_collection'),
82
+ claim_collection: z.strictObject({
64
83
  id: z.string().describe('Collection id, e.g. "8250".'),
65
84
  }),
66
- z.strictObject({
67
- kind: z.literal('claim_collections'),
68
- entity: z.string().optional().describe('Its did:ixo:entity:… DID.'),
69
- admin: account('Collection admin.'),
70
- first,
85
+ claim_collections: z.strictObject({
86
+ entity: z
87
+ .string()
88
+ .optional()
89
+ .describe(
90
+ "The collections' entity (did:ixo:entity:…); alone, every admin's.",
91
+ ),
92
+ admin: accountArg('Collection admin'),
93
+ first: firstArg,
71
94
  }),
72
- z.strictObject({
73
- kind: z.literal('entities'),
74
- owner: account('Entity owner.'),
75
- type: z.string().optional().describe('e.g. "dao".'),
76
- first,
95
+ entities: z.strictObject({
96
+ owner: accountArg('Entity owner'),
97
+ type: z.string().optional().describe('Entity type, e.g. "dao".'),
98
+ first: firstArg,
77
99
  }),
78
- z.strictObject({
79
- kind: z.literal('entity'),
80
- id: z.string().describe('did:ixo:entity:…'),
100
+ entity: z.strictObject({
101
+ id: z.string().describe('The entity DID: did:ixo:entity:<32 hex>.'),
81
102
  }),
82
- z.strictObject({
83
- kind: z.literal('did_document'),
103
+ did_document: z.strictObject({
84
104
  id: z
85
105
  .string()
86
106
  .optional()
87
- .describe("Any did:ixo:… DID; default: the user's."),
107
+ .describe("Any did:ixo:… DID. Default: the user's."),
88
108
  }),
89
- z.strictObject({
90
- kind: z.literal('token_balances'),
91
- address: account('Token holder.'),
92
- first,
109
+ token_balances: z.strictObject({
110
+ address: accountArg('Token holder'),
111
+ first: firstArg,
93
112
  }),
94
- z.strictObject({
95
- kind: z.literal('transactions'),
96
- address: account('Signer or recipient.'),
113
+ transactions: z.strictObject({
114
+ address: accountArg('Signer or recipient'),
97
115
  direction: z
98
116
  .enum(['signed', 'received'])
99
117
  .optional()
100
- .describe('Default: signed.'),
101
- first,
118
+ .describe('Signed by the address, or received by it. Default: signed.'),
119
+ first: firstArg,
102
120
  }),
103
- z.strictObject({
104
- kind: z.literal('transaction'),
105
- hash: z.string().describe('64-hex transaction hash.'),
121
+ transaction: z.strictObject({
122
+ hash: z.string().describe('The 64-hex transaction hash.'),
106
123
  }),
107
- ]);
124
+ };
108
125
 
109
- export type ChainRequest = z.infer<typeof chainRequestSchema>;
110
- export type ChainRequestKind = ChainRequest['kind'];
126
+ type CoreArgs = typeof CORE_ARGS;
127
+ type ChainRequestKind = keyof CoreArgs;
128
+ /** A core kind's request: its name beside its parsed arguments. */
129
+ export type ChainRequest = {
130
+ [K in ChainRequestKind]: { kind: K } & z.output<CoreArgs[K]>;
131
+ }[ChainRequestKind];
111
132
  export type Subject = 'user' | 'other';
112
133
 
113
- export const chainLookupSchema = z.strictObject({
114
- requests: z.array(chainRequestSchema).min(1).max(MAX_REQUESTS),
115
- });
116
-
117
134
  /**
118
- * One request as a document field, or why it cannot be asked. `subject` is
119
- * known before the read for a request by account; `first` is the rows a
120
- * list request reads.
135
+ * One request as a root field of the lookup document (without its alias),
136
+ * or why it cannot be asked. `subject` is known before the read for a
137
+ * request by account; `first` is the rows a list request reads.
121
138
  */
122
139
  export type FieldPlan =
123
- | ({
124
- ok: true;
125
- request: ChainRequest;
126
- kind: ChainRequestKind;
127
- subject?: Subject;
128
- first?: number;
129
- } & AliasedField)
130
- | {
131
- ok: false;
132
- request: ChainRequest;
133
- kind: ChainRequestKind;
134
- detail: string;
135
- };
140
+ | { ok: true; field: string; subject?: Subject; first?: number }
141
+ | { ok: false; detail: string };
136
142
 
137
143
  type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
138
144
 
@@ -151,8 +157,8 @@ export function literal(value: Json): string {
151
157
  }
152
158
 
153
159
  /**
154
- * The rows a list request reads: its `first` (1–20 by the schema, default
155
- * 10), capped at `max(5, floor(40 / count))` when the document holds
160
+ * The rows a list request reads: its `first` (1–20 by its kind's schema,
161
+ * default 10), capped at `max(5, floor(40 / count))` when the call holds
156
162
  * `count > 1` requests.
157
163
  */
158
164
  export function firstOf(requested: number | undefined, count: number): number {
@@ -163,10 +169,61 @@ export function firstOf(requested: number | undefined, count: number): number {
163
169
  return Math.min(requested ?? DEFAULT_FIRST, cap, MAX_FIRST);
164
170
  }
165
171
 
172
+ const BECH32_CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
173
+ const BECH32_GENERATOR = [
174
+ 0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3,
175
+ ];
176
+ /** The checksum's length in characters. */
177
+ const BECH32_CHECKSUM = 6;
178
+
179
+ /** BIP-173 `bech32_polymod`. */
180
+ function polymod(values: readonly number[]): number {
181
+ let checksum = 1;
182
+ for (const value of values) {
183
+ const top = checksum >>> 25;
184
+ checksum = ((checksum & 0x1ffffff) << 5) ^ value;
185
+ BECH32_GENERATOR.forEach((generator, bit) => {
186
+ if ((top >>> bit) & 1) checksum ^= generator;
187
+ });
188
+ }
189
+ return checksum;
190
+ }
191
+
192
+ /**
193
+ * Whether `address` is a valid lower-case bech32 string (BIP-173, as Cosmos
194
+ * SDK addresses are) with the prefix `prefix`: only bech32 characters after
195
+ * `<prefix>1`, the checksum matching, and data that converts to whole
196
+ * bytes (at most 4 padding bits, all zero). Its length is the caller's to
197
+ * bound.
198
+ */
199
+ export function isBech32(address: string, prefix: string): boolean {
200
+ if (!address.startsWith(`${prefix}1`)) return false;
201
+ const data = address.slice(prefix.length + 1);
202
+ if (data.length <= BECH32_CHECKSUM) return false;
203
+ const values: number[] = [];
204
+ for (const char of data) {
205
+ const value = BECH32_CHARSET.indexOf(char);
206
+ if (value < 0) return false;
207
+ values.push(value);
208
+ }
209
+ const codes = [...prefix].map((char) => char.charCodeAt(0));
210
+ const expanded = [
211
+ ...codes.map((code) => code >> 5),
212
+ 0,
213
+ ...codes.map((code) => code & 31),
214
+ ];
215
+ if (polymod([...expanded, ...values]) !== 1) return false;
216
+ const payload = values.length - BECH32_CHECKSUM;
217
+ const padding = (payload * 5) % 8;
218
+ const last = values[payload - 1] ?? 0;
219
+ return padding < 5 && (last & ((1 << padding) - 1)) === 0;
220
+ }
221
+
166
222
  /**
167
223
  * The account an account field names: the user's own address when the
168
224
  * field is omitted, else an `ixo1…` address or the address a
169
- * `did:ixo:ixo1…` DID names.
225
+ * `did:ixo:ixo1…` DID names, its bech32 checksum verified (a mistyped
226
+ * address is refused, never read).
170
227
  */
171
228
  export function accountOf(
172
229
  input: string | undefined,
@@ -187,6 +244,11 @@ export function accountOf(
187
244
  ok: false,
188
245
  detail: `${role} must be an ixo1… address or a did:ixo:ixo1… DID.`,
189
246
  };
247
+ if (!isBech32(address, 'ixo'))
248
+ return {
249
+ ok: false,
250
+ detail: `${role} is not a valid ixo1… address: its checksum does not match. Check it for a typo.`,
251
+ };
190
252
  return {
191
253
  ok: true,
192
254
  address,
@@ -215,30 +277,17 @@ function equalities(pairs: [string, string | undefined][]): string {
215
277
  );
216
278
  }
217
279
 
280
+ const invalid = (detail: string): FieldPlan => ({ ok: false, detail });
281
+
218
282
  /**
219
- * One request as an aliased field of the lookup document; `count` is the
220
- * number of requests the document holds.
283
+ * One request as a root field of the lookup document. `rows` is the rows a
284
+ * list request reads: its `first` as {@link firstOf} scales it for the call.
221
285
  */
222
286
  export function requestField(
223
- alias: string,
224
287
  request: ChainRequest,
225
288
  identity: UserIdentity,
226
- count = 1,
289
+ rows: number,
227
290
  ): FieldPlan {
228
- const plan = fieldOf(alias, request, identity, count);
229
- return 'detail' in plan
230
- ? { ok: false, request, kind: request.kind, detail: plan.detail }
231
- : { ok: true, request, kind: request.kind, alias, ...plan };
232
- }
233
-
234
- const invalid = (detail: string) => ({ detail });
235
-
236
- function fieldOf(
237
- alias: string,
238
- request: ChainRequest,
239
- identity: UserIdentity,
240
- count: number,
241
- ): { field: string; subject?: Subject; first?: number } | { detail: string } {
242
291
  switch (request.kind) {
243
292
  case 'claims': {
244
293
  const agent = accountOf(request.agent, identity, 'agent');
@@ -257,11 +306,11 @@ function fieldOf(
257
306
  ['collectionId', request.collectionId],
258
307
  ]);
259
308
  const after = request.after ? `, after: ${literal(request.after)}` : '';
260
- const n = firstOf(request.first, count);
261
309
  return {
310
+ ok: true,
262
311
  subject: agent.subject,
263
- first: n,
264
- field: `${alias}: claims(filter: ${filter}, first: ${n}${after}, orderBy: SUBMISSION_DATE_DESC) { totalCount pageInfo { hasNextPage endCursor } nodes { ${CLAIM_FIELDS} evaluation { status evaluationDate } } }`,
312
+ first: rows,
313
+ field: `claims(filter: ${filter}, first: ${rows}${after}, orderBy: SUBMISSION_DATE_DESC) { totalCount pageInfo { hasNextPage endCursor } nodes { ${CLAIM_FIELDS} evaluation { status evaluationDate } } }`,
265
314
  };
266
315
  }
267
316
  case 'claim':
@@ -270,13 +319,15 @@ function fieldOf(
270
319
  'claimId must be a claim id (a CID such as "bafkrei…").',
271
320
  );
272
321
  return {
273
- field: `${alias}: claim(claimId: ${literal(request.claimId)}) { ${CLAIM_FIELDS} evaluation { status evaluationDate oracle agentDid agentAddress reason amount } }`,
322
+ ok: true,
323
+ field: `claim(claimId: ${literal(request.claimId)}) { ${CLAIM_FIELDS} evaluation { status evaluationDate oracle agentDid agentAddress reason amount } }`,
274
324
  };
275
325
  case 'claim_collection':
276
326
  if (!COLLECTION_ID.test(request.id))
277
327
  return invalid('id must be a collection number such as "8250".');
278
328
  return {
279
- field: `${alias}: claimCollection(id: ${literal(request.id)}) { ${COLLECTION_FIELDS} payments }`,
329
+ ok: true,
330
+ field: `claimCollection(id: ${literal(request.id)}) { ${COLLECTION_FIELDS} payments }`,
280
331
  };
281
332
  case 'claim_collections': {
282
333
  if (request.entity !== undefined && !ENTITY_DID.test(request.entity))
@@ -292,15 +343,15 @@ function fieldOf(
292
343
  ['entity', request.entity],
293
344
  ['admin', admin?.address],
294
345
  ]);
295
- const n = firstOf(request.first, count);
296
346
  // Newest start date first (the id is text: "9" sorts above "10"). A
297
347
  // collection without a start date sorts first, as PostgreSQL puts
298
348
  // NULLs first in a descending order; x/claims reads the start date on
299
349
  // every claim submission, so a collection that takes claims has one.
300
350
  return {
351
+ ok: true,
301
352
  ...(admin ? { subject: admin.subject } : {}),
302
- first: n,
303
- field: `${alias}: claimCollections(filter: ${filter}, first: ${n}, orderBy: START_DATE_DESC) { totalCount nodes { ${COLLECTION_FIELDS} } }`,
353
+ first: rows,
354
+ field: `claimCollections(filter: ${filter}, first: ${rows}, orderBy: START_DATE_DESC) { totalCount nodes { ${COLLECTION_FIELDS} } }`,
304
355
  };
305
356
  }
306
357
  case 'entities': {
@@ -312,18 +363,19 @@ function fieldOf(
312
363
  ['owner', owner.address],
313
364
  ['type', request.type],
314
365
  ]);
315
- const n = firstOf(request.first, count);
316
366
  return {
367
+ ok: true,
317
368
  subject: owner.subject,
318
- first: n,
319
- field: `${alias}: entities(filter: ${filter}, first: ${n}, orderBy: ID_DESC) { totalCount nodes { id type status owner metadata } }`,
369
+ first: rows,
370
+ field: `entities(filter: ${filter}, first: ${rows}, orderBy: ID_DESC) { totalCount nodes { id type status owner metadata } }`,
320
371
  };
321
372
  }
322
373
  case 'entity':
323
374
  if (!ENTITY_DID.test(request.id))
324
375
  return invalid('id must be a did:ixo:entity:… DID.');
325
376
  return {
326
- field: `${alias}: entity(id: ${literal(request.id)}) { id type status owner accounts settings linkedResource linkedEntity controller service }`,
377
+ ok: true,
378
+ field: `entity(id: ${literal(request.id)}) { id type status owner accounts settings linkedResource linkedEntity controller service }`,
327
379
  };
328
380
  case 'did_document': {
329
381
  const id = request.id ?? identity.did;
@@ -334,68 +386,48 @@ function fieldOf(
334
386
  : 'id must be a did:ixo:… DID.',
335
387
  );
336
388
  return {
389
+ ok: true,
337
390
  subject: id === identity.did ? 'user' : 'other',
338
- field: `${alias}: iid(id: ${literal(id)}) { id controller verificationMethod linkedResource alsoKnownAs }`,
391
+ field: `iid(id: ${literal(id)}) { id controller verificationMethod linkedResource alsoKnownAs }`,
339
392
  };
340
393
  }
341
394
  case 'token_balances': {
342
395
  const holder = accountOf(request.address, identity, 'address');
343
396
  if (!holder.ok) return invalid(holder.detail);
344
- const n = firstOf(request.first, count);
345
397
  return {
398
+ ok: true,
346
399
  subject: holder.subject,
347
- first: n,
348
- field: `${alias}: tokenBalances(filter: ${equalities([['address', holder.address]])}, first: ${n}, orderBy: AMOUNT_DESC) { totalCount nodes { tokenId amount retired token { name collection tokenClassByName { contractAddress class } } } }`,
400
+ first: rows,
401
+ field: `tokenBalances(filter: ${equalities([['address', holder.address]])}, first: ${rows}, orderBy: AMOUNT_DESC) { totalCount nodes { tokenId amount retired token { name collection tokenClassByName { contractAddress class } } } }`,
349
402
  };
350
403
  }
351
404
  case 'transactions': {
352
405
  const holder = accountOf(request.address, identity, 'address');
353
406
  if (!holder.ok) return invalid(holder.detail);
354
- const n = firstOf(request.first, count);
355
407
  if (request.direction === 'received')
356
408
  // `Message.to` (indexed) is the recipient field of the message, or
357
409
  // of its first inner message for an authz MsgExec: toAddress,
358
410
  // receiver, recipient, recipientAddress or recipientDid. The same
359
411
  // filter the Portal's wallet history polls with.
360
412
  return {
413
+ ok: true,
361
414
  subject: holder.subject,
362
- first: n,
363
- field: `${alias}: messages(filter: ${equalities([['to', holder.address]])}, first: ${n}, orderBy: ID_DESC) { totalCount nodes { ${MESSAGE_FIELDS} transactionHash transactionByTransactionHash { ${TRANSACTION_FIELDS} } } }`,
415
+ first: rows,
416
+ field: `messages(filter: ${equalities([['to', holder.address]])}, first: ${rows}, orderBy: ID_DESC) { totalCount nodes { ${MESSAGE_FIELDS} transactionHash transactionByTransactionHash { ${TRANSACTION_FIELDS} } } }`,
364
417
  };
365
418
  return {
419
+ ok: true,
366
420
  subject: holder.subject,
367
- first: n,
368
- field: `${alias}: transactionSigners(filter: ${equalities([['signerAddress', holder.address]])}, first: ${n}, orderBy: TRANSACTION_ID_DESC) { totalCount nodes { transactionHash transaction { ${TRANSACTION_FIELDS} ${messagesOf(LIST_TX_MESSAGES)} } } }`,
421
+ first: rows,
422
+ field: `transactionSigners(filter: ${equalities([['signerAddress', holder.address]])}, first: ${rows}, orderBy: TRANSACTION_ID_DESC) { totalCount nodes { transactionHash transaction { ${TRANSACTION_FIELDS} ${messagesOf(LIST_TX_MESSAGES)} } } }`,
369
423
  };
370
424
  }
371
425
  case 'transaction':
372
426
  if (!TX_HASH.test(request.hash))
373
427
  return invalid('hash must be a 64-character hex transaction hash.');
374
428
  return {
375
- field: `${alias}: transactions(filter: ${equalities([['hash', request.hash.toUpperCase()]])}, first: 1) { nodes { ${TRANSACTION_FIELDS} fee transactionSignersByTransactionId(first: ${TX_SIGNERS}) { nodes { signerAddress } } ${messagesOf(ONE_TX_MESSAGES)} } }`,
429
+ ok: true,
430
+ field: `transactions(filter: ${equalities([['hash', request.hash.toUpperCase()]])}, first: 1) { nodes { ${TRANSACTION_FIELDS} fee transactionSignersByTransactionId(first: ${TX_SIGNERS}) { nodes { signerAddress } } ${messagesOf(ONE_TX_MESSAGES)} } }`,
376
431
  };
377
432
  }
378
433
  }
379
-
380
- /** The alias of the request at `index`. */
381
- const aliasOf = (index: number) => `r${index}`;
382
-
383
- /**
384
- * Every askable request as an aliased document field (aliases `r0…r7`, by
385
- * position) and each request's plan. `fields` is empty when no request can
386
- * be asked.
387
- */
388
- export function buildDocument(
389
- requests: readonly ChainRequest[],
390
- identity: UserIdentity,
391
- ): { fields: AliasedField[]; plans: FieldPlan[] } {
392
- const plans = requests.map((request, index) =>
393
- requestField(aliasOf(index), request, identity, requests.length),
394
- );
395
- return {
396
- fields: plans.flatMap((plan) =>
397
- plan.ok ? [{ alias: plan.alias, field: plan.field }] : [],
398
- ),
399
- plans,
400
- };
401
- }