@ixo/oracle-runtime-workers 0.32.0 → 0.33.1

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.
@@ -14,13 +14,15 @@
14
14
  * leads; that table holds one row per DAO, and a member of three groups
15
15
  * answered in 0.27 s on mainnet on Oct 29 2026. cw721 (NFT) staking is not read.
16
16
  * Each list is ordered by its primary key (voting module or contract
17
- * address).
17
+ * address). The `daos` kind also reads each DAO's first three proposal
18
+ * lists (`idx_dao_proposal_module_dao`) with their intake (a primary key),
19
+ * for new proposals.
18
20
  */
19
21
  import { z } from 'zod';
20
22
  import { displayCoin } from '../coins';
21
23
  import { defineKind, type KindContext, type KindResult } from '../types';
22
24
  import { accountArg, firstArg } from '../../queries';
23
- import { text } from '../../summaries';
25
+ import { isRecord, text } from '../../summaries';
24
26
  import {
25
27
  PAGE,
26
28
  accountFor,
@@ -32,6 +34,7 @@ import {
32
34
  idText,
33
35
  listResult,
34
36
  longText,
37
+ nonEmpty,
35
38
  num,
36
39
  opt,
37
40
  str,
@@ -219,6 +222,117 @@ const coreNode = z.looseObject({
219
222
  description: opt,
220
223
  });
221
224
 
225
+ /** Proposal lists read per DAO (single choice, multiple choice…). */
226
+ const LISTS_PER_DAO = 3;
227
+
228
+ /**
229
+ * A DAO's proposal lists: what a new proposal needs (who may propose, the
230
+ * deposit, the voting time) and what cards can do with the list. The
231
+ * `config`, `proposalCreationPolicy` and `depositInfo` columns hold the
232
+ * contracts' own query answers (DAO DAO v2: `{ allow_revoting,
233
+ * only_members_execute, max_voting_period, … }`, `{ anyone: {} }` or
234
+ * `{ module: { addr } }`, `{ denom: { native } | { cw20 }, amount,
235
+ * refund_policy }`).
236
+ */
237
+ const LIST_FIELDS = `daoProposalModulesByDaoAddress(first: ${LISTS_PER_DAO}, orderBy: PRIMARY_KEY_ASC) { ${PAGE} nodes { address prefix status moduleType proposalCreationPolicy config daoPreProposeModuleByPreProposeModule { depositInfo openProposalSubmission } } }`;
238
+ const listNode = z.looseObject({
239
+ address: str,
240
+ prefix: opt,
241
+ status: opt,
242
+ moduleType: opt,
243
+ proposalCreationPolicy: z.unknown().optional(),
244
+ config: z.unknown().optional(),
245
+ daoPreProposeModuleByPreProposeModule: z
246
+ .looseObject({
247
+ depositInfo: z.unknown().optional(),
248
+ openProposalSubmission: z.boolean().nullish(),
249
+ })
250
+ .nullish(),
251
+ });
252
+ const coreWithLists = coreNode.extend({
253
+ daoProposalModulesByDaoAddress: connection(listNode),
254
+ });
255
+ type CoreWithLists = z.infer<typeof coreWithLists>;
256
+
257
+ /** DAO DAO's single-choice proposal contract (the only kind cards vote on). */
258
+ const SINGLE_CHOICE = 'dao_proposal_single';
259
+ const REFUND_POLICIES = ['always', 'only_passed', 'never'] as const;
260
+
261
+ /** cw-utils `Duration` as words: `{ time: s }` or `{ height: n }`. */
262
+ function durationText(value: unknown): string | undefined {
263
+ if (!isRecord(value)) return undefined;
264
+ const count = (n: number, unit: string) =>
265
+ `${n} ${unit}${n === 1 ? '' : 's'}`;
266
+ if (typeof value.time === 'number' && Number.isSafeInteger(value.time)) {
267
+ const s = value.time;
268
+ if (s > 0 && s % 86_400 === 0) return count(s / 86_400, 'day');
269
+ if (s > 0 && s % 3_600 === 0) return count(s / 3_600, 'hour');
270
+ if (s > 0 && s % 60 === 0) return count(s / 60, 'minute');
271
+ return count(s, 'second');
272
+ }
273
+ if (typeof value.height === 'number' && Number.isSafeInteger(value.height))
274
+ return count(value.height, 'block');
275
+ return undefined;
276
+ }
277
+
278
+ /**
279
+ * A proposal deposit (`CheckedDepositInfo`): a bank coin as shown, or a
280
+ * cw20 amount with `cardCannotPay` (cards pay bank coins only), each with
281
+ * its refund rule; undefined when there is none or it is unreadable.
282
+ */
283
+ async function depositOf(ctx: KindContext, value: unknown) {
284
+ if (!isRecord(value) || !isRecord(value.denom)) return undefined;
285
+ const refundPolicy = REFUND_POLICIES.find(
286
+ (policy) => policy === value.refund_policy,
287
+ );
288
+ if (typeof value.denom.native === 'string') {
289
+ const coin = chainCoin({ denom: value.denom.native, amount: value.amount });
290
+ const shown = coin ? await displayCoin(ctx, coin) : null;
291
+ return shown ? compact({ ...shown, refundPolicy }) : undefined;
292
+ }
293
+ const amountBaseUnits = amountText(value.amount);
294
+ return typeof value.denom.cw20 === 'string' && amountBaseUnits
295
+ ? compact({
296
+ token: idText(value.denom.cw20),
297
+ amountBaseUnits,
298
+ refundPolicy,
299
+ cardCannotPay: true,
300
+ })
301
+ : undefined;
302
+ }
303
+
304
+ /** One proposal list of a DAO as the `daos` answer shows it. */
305
+ async function proposalListOf(
306
+ ctx: KindContext,
307
+ list: z.infer<typeof listNode>,
308
+ ) {
309
+ const config = isRecord(list.config) ? list.config : {};
310
+ const policy = isRecord(list.proposalCreationPolicy)
311
+ ? list.proposalCreationPolicy
312
+ : {};
313
+ const intake = list.daoPreProposeModuleByPreProposeModule;
314
+ // `anyone`: proposals go straight to the list, from anyone, without a
315
+ // deposit; `module`: through the list's intake, which holds both rules.
316
+ const viaIntake = isRecord(policy.module);
317
+ return compact({
318
+ proposalModule: idText(list.address),
319
+ prefix: text(list.prefix, 20),
320
+ disabled: list.status === 'disabled' ? true : undefined,
321
+ multipleChoice:
322
+ list.moduleType && list.moduleType !== SINGLE_CHOICE ? true : undefined,
323
+ membersOnly:
324
+ viaIntake && intake?.openProposalSubmission === false ? true : undefined,
325
+ deposit: viaIntake ? await depositOf(ctx, intake?.depositInfo) : undefined,
326
+ votingTime: durationText(config.max_voting_period),
327
+ allowRevoting:
328
+ typeof config.allow_revoting === 'boolean'
329
+ ? config.allow_revoting
330
+ : undefined,
331
+ membersOnlyCarryOut:
332
+ config.only_members_execute === true ? true : undefined,
333
+ });
334
+ }
335
+
222
336
  /** What a DAO row shows of its core. */
223
337
  type CoreText = {
224
338
  address: string;
@@ -226,7 +340,11 @@ type CoreText = {
226
340
  description?: string | null;
227
341
  };
228
342
 
229
- async function daoOf(ctx: KindContext, entry: Membership<CoreText>) {
343
+ async function daoOf<C extends CoreText>(
344
+ ctx: KindContext,
345
+ entry: Membership<C>,
346
+ extra?: (core: C) => Promise<Record<string, unknown>>,
347
+ ) {
230
348
  const native =
231
349
  entry.membership === 'native-staked' && entry.nativeDenom
232
350
  ? chainCoin({ denom: entry.nativeDenom, amount: entry.weight })
@@ -255,38 +373,56 @@ async function daoOf(ctx: KindContext, entry: Membership<CoreText>) {
255
373
  votingPowerShare: shareOf(entry.weight, entry.totalWeight),
256
374
  votingModule: idText(entry.votingModule),
257
375
  moduleType: text(entry.moduleType, 80),
376
+ ...(extra ? await extra(entry.core) : {}),
258
377
  });
259
378
  }
260
379
 
261
380
  /**
262
381
  * The `daos` answer's data from the membership answer: the first `n`
263
- * DAOs, and `more` when the index holds further memberships.
382
+ * DAOs, and `more` when the index holds further memberships. `extra` adds
383
+ * fields read with each DAO core (the `daos` kind's proposal lists).
264
384
  */
265
385
  export async function daosData<C extends CoreText>(
266
386
  ctx: KindContext,
267
387
  answer: MembershipRows<C>,
268
388
  n: number,
389
+ extra?: (core: C) => Promise<Record<string, unknown>>,
269
390
  ): Promise<Record<string, unknown>> {
270
391
  const all = membershipsOf(answer);
271
392
  return compact({
272
- items: await Promise.all(all.slice(0, n).map((entry) => daoOf(ctx, entry))),
393
+ items: await Promise.all(
394
+ all.slice(0, n).map((entry) => daoOf(ctx, entry, extra)),
395
+ ),
273
396
  more: all.length > n || moreMemberships(answer) ? true : undefined,
274
397
  });
275
398
  }
276
399
 
277
- const answerSchema = membershipSchema(coreNode);
400
+ /** A DAO core's proposal lists, and `moreProposalLists` past those read. */
401
+ async function listsOf(ctx: KindContext, core: CoreWithLists) {
402
+ const lists = core.daoProposalModulesByDaoAddress;
403
+ return {
404
+ proposalLists: nonEmpty(
405
+ await Promise.all(lists.nodes.map((list) => proposalListOf(ctx, list))),
406
+ ),
407
+ moreProposalLists: lists.pageInfo?.hasNextPage ? true : undefined,
408
+ };
409
+ }
410
+
411
+ const answerSchema = membershipSchema(coreWithLists);
278
412
 
279
413
  export const daos = defineKind({
280
414
  name: 'daos',
281
415
  summary:
282
- 'DAOs an account (default: the user) is a member of, how (staked or group member) and its share of the voting power.',
416
+ 'DAOs an account (default: the user) is a member of, how, its share of the voting power, and where each takes new proposals.',
283
417
  explain: [
284
418
  'A DAO (DAO DAO on the IXO chain) decides by proposals and votes; its voting module sets who votes and with what weight.',
285
419
  '`membership`: `cw4-member` (a group member with a fixed `weight`), `cw20-staked` (voting power from cw20 tokens staked in the DAO, `staked.amountBaseUnits` of the `staked.token` contract) or `native-staked` (from staked bank tokens, e.g. IXO; `staked` is a bank coin, `amount` in display units beside `symbol`).',
286
420
  '`votingPowerShare` is the account’s weight or stake over the voting module’s total, rounded down; staked tokens count only while staked, and unstaking may take a waiting period.',
287
421
  'Each: dao (the DAO core contract address), name, description, membership, staked or weight, votingPowerShare, votingModule.',
288
- '`addedByGroupAdmin: true` (every cw4-member row): a cw4 group’s admin can add any address to the group without its consent, so membership alone does not mean the account joined, agreed or knows of the DAO.',
289
- '`nameIsSelfChosen: true`: the name and description are whatever the DAO set for itself, never a verified identity or an endorsement; say so before relying on them.',
422
+ '`addedByGroupAdmin: true` (every cw4-member row): members of this kind of group are added by its admin. That is normal: leave it out of an ordinary answer and never suggest the person did not join; mention it briefly only when it matters, for example when they do not recognise a group (“Members of this group are added by its admin.”).',
423
+ '`nameIsSelfChosen: true`: the group chose its own name and description. Mention it only when it matters (for example when they rely on the name to tell who runs the group), in one short, calm note such as “Group names are chosen by the group itself.”',
424
+ '`dao` is also the group’s own account: what the group holds (chain_lookup balances with that address) and what acts when one of its proposals passes.',
425
+ '`proposalLists` (at most 3; `moreProposalLists: true` past them): where the DAO takes proposals, each `proposalModule` (its address), `prefix` (the letter before proposal numbers, A in A3), `votingTime`, `allowRevoting` (whether a vote can be changed until voting closes), `membersOnly: true` (only members may propose), `membersOnlyCarryOut: true` (only members may carry out a passed proposal), and `deposit` when proposing takes one: a bank coin (`amount` beside `symbol`) or a cw20 amount with `cardCannotPay: true`, with `refundPolicy` (`only_passed`: back only if it passes and is carried out, `always`: back when carried out or closed, `never`: not back). `multipleChoice: true`: Auth Hub cards cannot use that list. `disabled: true`: the group switched it off: no new proposals through it, and its proposals with actions can’t be carried out (votes and closes still work).',
290
426
  'NFT (cw721) staking is not indexed here. Ask `dao_proposals` for what the DAOs are voting on.',
291
427
  ].join(' '),
292
428
  source: 'blocksync',
@@ -300,7 +436,7 @@ export const daos = defineKind({
300
436
  const subfields = membershipFields(
301
437
  member.address,
302
438
  n,
303
- 'address name description',
439
+ `address name description ${LIST_FIELDS}`,
304
440
  );
305
441
  return {
306
442
  source: 'blocksync',
@@ -309,7 +445,7 @@ export const daos = defineKind({
309
445
  const parsed = answerSchema.safeParse(raw);
310
446
  if (!parsed.success) return { ok: false, ...unexpected };
311
447
  return listResult(
312
- await daosData(ctx, parsed.data, n),
448
+ await daosData(ctx, parsed.data, n, (core) => listsOf(ctx, core)),
313
449
  n,
314
450
  member.subject,
315
451
  );