@ixo/oracle-runtime-workers 0.32.0 → 0.33.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 +4 -4
- 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 +68 -34
- 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 +219 -7
- package/src/plugins/hub-actions/hub-actions.plugin.ts +188 -31
- package/src/plugins/ixo-chain/kinds/indexed/dao-proposals.ts +37 -2
- package/src/plugins/ixo-chain/kinds/indexed/daos.ts +145 -9
|
@@ -56,6 +56,8 @@ const DAODAO_VOTES_FROM: Record<ChainCall['network'], number> = {
|
|
|
56
56
|
|
|
57
57
|
/** Proposal modules read per DAO (single choice, multiple choice…). */
|
|
58
58
|
const MODULES_PER_DAO = 3;
|
|
59
|
+
/** DAO DAO's single-choice proposal contract (the only kind cards act on). */
|
|
60
|
+
const SINGLE_CHOICE = 'dao_proposal_single';
|
|
59
61
|
/** DAOs read per kind of membership when no DAO is named. */
|
|
60
62
|
const DAOS_PER_MEMBERSHIP = 3;
|
|
61
63
|
|
|
@@ -85,6 +87,7 @@ const proposalNode = z.looseObject({
|
|
|
85
87
|
expiration: z.unknown().optional(),
|
|
86
88
|
totalPower: opt,
|
|
87
89
|
votes: z.unknown().optional(),
|
|
90
|
+
allowRevoting: z.boolean().nullish(),
|
|
88
91
|
daoVotesByProposalModuleAndProposalId: z
|
|
89
92
|
.object({ nodes: z.array(voteNode) })
|
|
90
93
|
.optional(),
|
|
@@ -92,6 +95,8 @@ const proposalNode = z.looseObject({
|
|
|
92
95
|
const moduleNode = z.looseObject({
|
|
93
96
|
address: str,
|
|
94
97
|
prefix: opt,
|
|
98
|
+
status: opt,
|
|
99
|
+
moduleType: opt,
|
|
95
100
|
daoProposalsByProposalModule: connection(proposalNode),
|
|
96
101
|
});
|
|
97
102
|
const coreNode = z.looseObject({
|
|
@@ -141,12 +146,24 @@ function rowsOf(
|
|
|
141
146
|
for (const module of modules.nodes) {
|
|
142
147
|
const proposals = module.daoProposalsByProposalModule;
|
|
143
148
|
if (proposals.pageInfo?.hasNextPage) more = true;
|
|
149
|
+
// Cards work only on a single-choice list. On a list the group
|
|
150
|
+
// switched off, votes and closes still work, but a proposal with
|
|
151
|
+
// actions cannot be carried out (Auth Hub's `list_disabled`), and this
|
|
152
|
+
// index row does not say whether a proposal has actions, so `canRun` is
|
|
153
|
+
// left out there. Auth Hub checks the contracts again.
|
|
154
|
+
const multipleChoice =
|
|
155
|
+
!!module.moduleType && module.moduleType !== SINGLE_CHOICE;
|
|
156
|
+
const listDisabled = module.status === 'disabled';
|
|
144
157
|
for (const proposal of proposals.nodes) {
|
|
145
158
|
const id = String(proposal.id);
|
|
146
159
|
const vote = proposal.daoVotesByProposalModuleAndProposalId?.nodes[0];
|
|
147
160
|
const indexedFromStart =
|
|
148
161
|
typeof proposal.startHeight === 'number' &&
|
|
149
162
|
proposal.startHeight >= votesFrom;
|
|
163
|
+
const expiry = expirationOf(proposal.expiration, now);
|
|
164
|
+
// The index keeps `open` until a proposal is carried out or closed:
|
|
165
|
+
// once voting has ended it may have passed or not.
|
|
166
|
+
const ended = proposal.status === 'open' && expiry.votingEnded === true;
|
|
150
167
|
rows.push({
|
|
151
168
|
startHeight: proposal.startHeight ?? -1,
|
|
152
169
|
id,
|
|
@@ -158,7 +175,23 @@ function rowsOf(
|
|
|
158
175
|
proposalModule: idText(module.address),
|
|
159
176
|
title: text(proposal.title),
|
|
160
177
|
status: text(proposal.status, 40),
|
|
161
|
-
...
|
|
178
|
+
...expiry,
|
|
179
|
+
allowRevoting:
|
|
180
|
+
typeof proposal.allowRevoting === 'boolean'
|
|
181
|
+
? proposal.allowRevoting
|
|
182
|
+
: undefined,
|
|
183
|
+
canRun:
|
|
184
|
+
!multipleChoice &&
|
|
185
|
+
!listDisabled &&
|
|
186
|
+
(proposal.status === 'passed' || ended)
|
|
187
|
+
? true
|
|
188
|
+
: undefined,
|
|
189
|
+
canClose:
|
|
190
|
+
!multipleChoice && (proposal.status === 'rejected' || ended)
|
|
191
|
+
? true
|
|
192
|
+
: undefined,
|
|
193
|
+
multipleChoice: multipleChoice ? true : undefined,
|
|
194
|
+
listDisabled: listDisabled ? true : undefined,
|
|
162
195
|
proposer: idText(proposal.proposer),
|
|
163
196
|
tally: tallyOf(proposal.votes),
|
|
164
197
|
totalPower: amountText(proposal.totalPower),
|
|
@@ -195,7 +228,7 @@ function coreFields(
|
|
|
195
228
|
const votes = voter
|
|
196
229
|
? ` daoVotesByProposalModuleAndProposalId(filter: ${equalTo('voter', voter)}, first: 1) { nodes { vote power votedAt } }`
|
|
197
230
|
: '';
|
|
198
|
-
return `address name${described ? ' description' : ''} daoProposalModulesByDaoAddress(first: ${MODULES_PER_DAO}, orderBy: PRIMARY_KEY_ASC) { ${PAGE} nodes { address prefix daoProposalsByProposalModule(${filter}first: ${n}, orderBy: PRIMARY_KEY_DESC) { ${PAGE} nodes { id title status proposer startHeight expiration totalPower votes${votes} } } } }`;
|
|
231
|
+
return `address name${described ? ' description' : ''} daoProposalModulesByDaoAddress(first: ${MODULES_PER_DAO}, orderBy: PRIMARY_KEY_ASC) { ${PAGE} nodes { address prefix status moduleType daoProposalsByProposalModule(${filter}first: ${n}, orderBy: PRIMARY_KEY_DESC) { ${PAGE} nodes { id title status proposer startHeight expiration totalPower votes allowRevoting${votes} } } } }`;
|
|
199
232
|
}
|
|
200
233
|
|
|
201
234
|
/** Rows by start height, newest first (a proposal without one last), then by id. */
|
|
@@ -242,6 +275,8 @@ export const daoProposals = defineKind({
|
|
|
242
275
|
'Each: dao, daoName, id and `label` (the DAO’s prefix and id, e.g. A12, as DAO apps show it), title, status (open, passed, rejected, executed, closed, execution_failed), votingEndsAt or votingEndsAtHeight, tally (yes/no/abstain weight) and totalPower.',
|
|
243
276
|
'`votingEnded: true` on an `open` proposal: its voting time is over and the index keeps `open` until it is executed or closed, so it may already have passed or failed.',
|
|
244
277
|
'`yourVote` (vote, power, votedAt), or `notVoted: true`, or `voteUnknown: true` when the proposal started before the index kept votes (votes cast before then are missing).',
|
|
278
|
+
'`allowRevoting`: whether a vote on it can be changed until voting closes. `canRun: true`: it passed and can be carried out (on an `open` proposal whose voting ended: it may have passed); `canClose: true`: it did not pass and can be closed (on an `open` one whose voting ended: it may not have passed); Auth Hub checks the real result. `multipleChoice: true`: Auth Hub cards cannot vote on, carry out or close it. `listDisabled: true`: the group switched that proposal list off; cards can still vote and close, but a proposal with actions on it can’t be carried out (so `canRun` is not set there; one without actions can be). Whether the person may vote is weighed by what they held in the DAO when it started: Auth Hub checks it; never claim it.',
|
|
279
|
+
'For cards copy `dao`, `proposalModule` and `id` exactly (not the label). `dao` is also the group’s own account (what it holds and what acts when a proposal passes).',
|
|
245
280
|
'Without `dao`, the DAOs of `address` (default: the user), at most 3 per kind of membership (see `daos`); `more: true` when there are more proposals or DAOs. Newest start first.',
|
|
246
281
|
'Titles are written by the proposer and DAO names by the DAO: what they claim, never an endorsement or a verified fact.',
|
|
247
282
|
].join(' '),
|
|
@@ -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,31 +373,47 @@ 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`).',
|
|
@@ -287,6 +421,8 @@ export const daos = defineKind({
|
|
|
287
421
|
'Each: dao (the DAO core contract address), name, description, membership, staked or weight, votingPowerShare, votingModule.',
|
|
288
422
|
'`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
423
|
'`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.',
|
|
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
|
);
|