@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.
- package/package.json +2 -2
- package/src/do/user-oracle-do.ts +5 -7
- package/src/hub-actions/chain-tx-refusals.ts +93 -3
- package/src/hub-actions/client.ts +119 -39
- package/src/hub-actions/transfer.ts +124 -20
- package/src/plugin-api/types.ts +55 -18
- package/src/plugins/hub-actions/chain-tx-catalog.ts +239 -11
- package/src/plugins/hub-actions/hub-actions.plugin.ts +188 -31
- package/src/plugins/ixo-chain/kinds/indexed/dao-proposals.ts +38 -3
- package/src/plugins/ixo-chain/kinds/indexed/daos.ts +147 -11
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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):
|
|
289
|
-
'`nameIsSelfChosen: true`: the name and description
|
|
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
|
-
|
|
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
|
);
|