@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CHAIN_TX_GROUP_MESSAGE_TYPES,
|
|
2
3
|
CHAIN_TX_MAX_MESSAGES,
|
|
3
4
|
CHAIN_TX_MESSAGE_TYPES,
|
|
4
5
|
operationJsonSchema,
|
|
@@ -30,6 +31,8 @@ import { toolNotRunMessage } from '../../core/tool-not-run';
|
|
|
30
31
|
import {
|
|
31
32
|
CHAIN_TX_CATALOG,
|
|
32
33
|
CHAIN_TX_GENERAL_RULES,
|
|
34
|
+
GROUP_ACTION_SCHEMA,
|
|
35
|
+
GROUP_ACTION_TYPES,
|
|
33
36
|
chainTxMessageInput,
|
|
34
37
|
hasUnsafeText,
|
|
35
38
|
} from './chain-tx-catalog';
|
|
@@ -100,22 +103,38 @@ const memoSchema = z
|
|
|
100
103
|
/**
|
|
101
104
|
* A catalogue field reduced to its basic type: no descriptions, patterns or
|
|
102
105
|
* limits (those stay in describe_chain_messages and the handler's check).
|
|
106
|
+
* A union of message types (a new group proposal's `actions`) is given as
|
|
107
|
+
* `typeUnion` gives the messages themselves: fields only for the types in
|
|
108
|
+
* `described`.
|
|
103
109
|
*/
|
|
104
|
-
export function fieldsOnly(
|
|
110
|
+
export function fieldsOnly(
|
|
111
|
+
schema: z.core.$ZodType,
|
|
112
|
+
described?: ReadonlySet<string>,
|
|
113
|
+
): z.ZodType {
|
|
105
114
|
if (schema instanceof z.ZodObject)
|
|
106
115
|
return z.strictObject(
|
|
107
116
|
Object.fromEntries(
|
|
108
117
|
Object.entries(schema.shape).map(([key, field]) => [
|
|
109
118
|
key,
|
|
110
|
-
fieldsOnly(field),
|
|
119
|
+
fieldsOnly(field, described),
|
|
111
120
|
]),
|
|
112
121
|
),
|
|
113
122
|
);
|
|
114
123
|
if (schema instanceof z.ZodOptional)
|
|
115
|
-
return fieldsOnly(schema.unwrap()).optional();
|
|
124
|
+
return fieldsOnly(schema.unwrap(), described).optional();
|
|
116
125
|
if (schema instanceof z.ZodNullable)
|
|
117
|
-
return fieldsOnly(schema.unwrap()).nullable();
|
|
118
|
-
if (schema instanceof z.ZodArray)
|
|
126
|
+
return fieldsOnly(schema.unwrap(), described).nullable();
|
|
127
|
+
if (schema instanceof z.ZodArray)
|
|
128
|
+
return z.array(fieldsOnly(schema.element, described));
|
|
129
|
+
if (schema instanceof z.ZodDiscriminatedUnion)
|
|
130
|
+
return typeUnion(
|
|
131
|
+
schema.options.map((option) => {
|
|
132
|
+
if (!(option instanceof z.ZodObject))
|
|
133
|
+
throw new Error('A message union holds a non-object branch');
|
|
134
|
+
return [messageTypeOf(option), option] as const;
|
|
135
|
+
}),
|
|
136
|
+
described,
|
|
137
|
+
);
|
|
119
138
|
if (schema instanceof z.ZodString) return z.string();
|
|
120
139
|
if (schema instanceof z.ZodNumber) return z.number();
|
|
121
140
|
if (schema instanceof z.ZodBoolean) return z.boolean();
|
|
@@ -125,42 +144,51 @@ export function fieldsOnly(schema: z.core.$ZodType): z.ZodType {
|
|
|
125
144
|
);
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
/** The `type` a catalogue message schema is for (its `type` literal). */
|
|
148
|
+
function messageTypeOf(schema: z.ZodObject): string {
|
|
149
|
+
const type = schema.shape.type;
|
|
150
|
+
const [value] = type instanceof z.ZodLiteral ? [...type.values] : [];
|
|
151
|
+
if (typeof value !== 'string')
|
|
152
|
+
throw new Error('A catalogue message schema has no `type` literal');
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
/**
|
|
129
157
|
* One message type as the model is given it: its field names and basic
|
|
130
158
|
* types, so a provider that holds a tool call to its schema sends every
|
|
131
159
|
* field. Loose, so a field the type does not have reaches the handler,
|
|
132
160
|
* which names it.
|
|
133
161
|
*/
|
|
134
|
-
function messageFieldsOnly(
|
|
162
|
+
function messageFieldsOnly(
|
|
163
|
+
schema: z.ZodObject,
|
|
164
|
+
described?: ReadonlySet<string>,
|
|
165
|
+
) {
|
|
135
166
|
return z.looseObject(
|
|
136
167
|
Object.fromEntries(
|
|
137
168
|
Object.entries(schema.shape).map(([key, field]) => [
|
|
138
169
|
key,
|
|
139
|
-
fieldsOnly(field),
|
|
170
|
+
fieldsOnly(field, described),
|
|
140
171
|
]),
|
|
141
172
|
),
|
|
142
173
|
);
|
|
143
174
|
}
|
|
144
175
|
|
|
145
176
|
/**
|
|
146
|
-
*
|
|
147
|
-
* when the type was described in this session
|
|
148
|
-
* type), and one open branch naming every
|
|
149
|
-
* any other field allowed). A
|
|
150
|
-
*
|
|
151
|
-
* the next model call is given its fields (`chainTxFieldsMiddleware`).
|
|
152
|
-
* Each message is checked by the handler against its full catalogue
|
|
153
|
-
* schema.
|
|
177
|
+
* Messages as the model is given them: per type its fields
|
|
178
|
+
* (messageFieldsOnly) when the type was described in this session
|
|
179
|
+
* (`described`; absent: every type), and one open branch naming every
|
|
180
|
+
* other type (`type` one of them, any other field allowed). A union inside
|
|
181
|
+
* a message (a new group proposal's `actions`) takes `nested` instead.
|
|
154
182
|
*/
|
|
155
|
-
|
|
156
|
-
|
|
183
|
+
function typeUnion(
|
|
184
|
+
schemas: readonly (readonly [string, z.ZodObject])[],
|
|
157
185
|
described?: ReadonlySet<string>,
|
|
186
|
+
nested: ReadonlySet<string> | undefined = described,
|
|
158
187
|
) {
|
|
159
|
-
const
|
|
160
|
-
const withFields = entries
|
|
188
|
+
const withFields = schemas
|
|
161
189
|
.filter(([type]) => !described || described.has(type))
|
|
162
|
-
.map(([,
|
|
163
|
-
const [firstUnread, ...unread] =
|
|
190
|
+
.map(([, schema]) => messageFieldsOnly(schema, nested));
|
|
191
|
+
const [firstUnread, ...unread] = schemas
|
|
164
192
|
.filter(([type]) => described && !described.has(type))
|
|
165
193
|
.map(([type]) => type);
|
|
166
194
|
const branches =
|
|
@@ -172,9 +200,36 @@ export function chainTransactionSchemaFor(
|
|
|
172
200
|
];
|
|
173
201
|
const [first, ...rest] = branches;
|
|
174
202
|
if (!first) throw new Error('The chain transaction catalogue is empty');
|
|
203
|
+
return z.discriminatedUnion('type', [first, ...rest]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The schema the model is given: the messages as `typeUnion` gives them,
|
|
208
|
+
* a new group proposal's actions the same way. A type the model has not
|
|
209
|
+
* read about costs its name only; the request handler refuses it until it
|
|
210
|
+
* is described, and the next model call is given its fields
|
|
211
|
+
* (`chainTxFieldsMiddleware`). Without a session store (`described`
|
|
212
|
+
* absent) every message type carries its fields, but a new proposal's
|
|
213
|
+
* actions are named only: the 14 action types' fields would repeat their
|
|
214
|
+
* top-level branches (26 228 characters instead of 20 950); the
|
|
215
|
+
* handler names any field it misses. Each message is checked by the
|
|
216
|
+
* handler against its full catalogue schema.
|
|
217
|
+
*/
|
|
218
|
+
export function chainTransactionSchemaFor(
|
|
219
|
+
catalog: Readonly<Record<string, { schema: z.ZodObject }>>,
|
|
220
|
+
described?: ReadonlySet<string>,
|
|
221
|
+
) {
|
|
175
222
|
return z.strictObject({
|
|
176
223
|
messages: z
|
|
177
|
-
.array(
|
|
224
|
+
.array(
|
|
225
|
+
typeUnion(
|
|
226
|
+
Object.entries(catalog).map(
|
|
227
|
+
([type, entry]) => [type, entry.schema] as const,
|
|
228
|
+
),
|
|
229
|
+
described,
|
|
230
|
+
described ?? new Set<string>(),
|
|
231
|
+
),
|
|
232
|
+
)
|
|
178
233
|
.min(1)
|
|
179
234
|
.max(CHAIN_TX_MAX_MESSAGES),
|
|
180
235
|
memo: memoSchema,
|
|
@@ -649,7 +704,10 @@ async function requestChainTransaction(
|
|
|
649
704
|
const unread = [
|
|
650
705
|
...new Set(
|
|
651
706
|
parsed.data.messages
|
|
652
|
-
.
|
|
707
|
+
.flatMap((message) => [
|
|
708
|
+
chainTxTypeOf(message),
|
|
709
|
+
...groupActionTypesOf(message),
|
|
710
|
+
])
|
|
653
711
|
.filter(
|
|
654
712
|
(type): type is ChainTxMessageType =>
|
|
655
713
|
type !== null && !described.includes(type),
|
|
@@ -719,6 +777,19 @@ function strictChainTxMessages(
|
|
|
719
777
|
): { messages: ChainTxMessageInput[] } | { issues: string[] } {
|
|
720
778
|
const checked: ChainTxMessageInput[] = [];
|
|
721
779
|
const issues: string[] = [];
|
|
780
|
+
// A group step is a chain of reads in Auth Hub and names a new
|
|
781
|
+
// proposal's number on its receipt: it goes alone on its card (the
|
|
782
|
+
// protocol refuses `messages` otherwise).
|
|
783
|
+
if (
|
|
784
|
+
messages.length > 1 &&
|
|
785
|
+
messages.some((message) => {
|
|
786
|
+
const type = chainTxTypeOf(message);
|
|
787
|
+
return type !== null && GROUP_STEP_TYPES.includes(type);
|
|
788
|
+
})
|
|
789
|
+
)
|
|
790
|
+
issues.push(
|
|
791
|
+
'messages: a group step (a group vote, carrying out or closing a group proposal, a new group proposal) goes alone on its card; prepare the other messages on another card',
|
|
792
|
+
);
|
|
722
793
|
messages.forEach((message, index) => {
|
|
723
794
|
const type = chainTxTypeOf(message);
|
|
724
795
|
if (!type) {
|
|
@@ -739,6 +810,31 @@ function chainTxTypeOf(message: unknown): ChainTxMessageType | null {
|
|
|
739
810
|
return CHAIN_TX_MESSAGE_TYPES.find((known) => known === type) ?? null;
|
|
740
811
|
}
|
|
741
812
|
|
|
813
|
+
const GROUP_STEP_TYPES: readonly ChainTxMessageType[] =
|
|
814
|
+
CHAIN_TX_GROUP_MESSAGE_TYPES;
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* The action types a new group proposal holds (each a type a group may
|
|
818
|
+
* do): their rules are read like a top-level message's before the card.
|
|
819
|
+
* Anything else is left to the message's own check.
|
|
820
|
+
*/
|
|
821
|
+
function groupActionTypesOf(message: unknown): ChainTxMessageType[] {
|
|
822
|
+
if (
|
|
823
|
+
chainTxTypeOf(message) !== 'daodao.proposal.create' ||
|
|
824
|
+
typeof message !== 'object' ||
|
|
825
|
+
message === null ||
|
|
826
|
+
!('actions' in message) ||
|
|
827
|
+
!Array.isArray(message.actions)
|
|
828
|
+
)
|
|
829
|
+
return [];
|
|
830
|
+
return message.actions
|
|
831
|
+
.map(chainTxTypeOf)
|
|
832
|
+
.filter(
|
|
833
|
+
(type): type is ChainTxMessageType =>
|
|
834
|
+
type !== null && GROUP_ACTION_TYPES.some((allowed) => allowed === type),
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
|
|
742
838
|
function chainTxShapeRefusal(
|
|
743
839
|
issues: readonly string[],
|
|
744
840
|
): HubActionRequestResult {
|
|
@@ -784,10 +880,11 @@ function issuesOf(
|
|
|
784
880
|
|
|
785
881
|
/**
|
|
786
882
|
* The catalogue types this account may use: the Hub capabilities'
|
|
787
|
-
* `chainTx.messageTypes
|
|
883
|
+
* `chainTx.messageTypes`, which follow the protocol level the Hub speaks
|
|
884
|
+
* with this runtime (level 5: the four 0.6.0 types; 6: 33; 7: all 37, the
|
|
885
|
+
* four group steps included). None when chain transactions cannot be used
|
|
788
886
|
* here: not a channel turn, Hub actions off, chain transactions off for the
|
|
789
|
-
* account,
|
|
790
|
-
* them), or capabilities that cannot be read (the owner object answers its
|
|
887
|
+
* account, or capabilities that cannot be read (the owner object answers its
|
|
791
888
|
* last good capabilities while a read fails, and does not retry a failed
|
|
792
889
|
* read for 30 s, so this never holds a turn for long).
|
|
793
890
|
*/
|
|
@@ -815,6 +912,19 @@ async function availableChainTxTypes(
|
|
|
815
912
|
);
|
|
816
913
|
}
|
|
817
914
|
|
|
915
|
+
/**
|
|
916
|
+
* A new group proposal's action as describe_chain_messages lists it: the
|
|
917
|
+
* types it may be, each with its own fields (described on its own), so one
|
|
918
|
+
* description does not repeat fourteen others.
|
|
919
|
+
*/
|
|
920
|
+
const GROUP_ACTION_FIELDS = {
|
|
921
|
+
type: 'object',
|
|
922
|
+
description:
|
|
923
|
+
'One message of the types listed in `type`, with exactly that type’s fields (describe_chain_messages that type); the group’s own account is its actor.',
|
|
924
|
+
properties: { type: { type: 'string', enum: [...GROUP_ACTION_TYPES] } },
|
|
925
|
+
required: ['type'],
|
|
926
|
+
};
|
|
927
|
+
|
|
818
928
|
/** Each type's fields as JSON Schema, built once. */
|
|
819
929
|
const catalogueFields = new Map<ChainTxMessageType, unknown>();
|
|
820
930
|
function fieldsOf(type: ChainTxMessageType): unknown {
|
|
@@ -822,6 +932,14 @@ function fieldsOf(type: ChainTxMessageType): unknown {
|
|
|
822
932
|
if (fields === undefined) {
|
|
823
933
|
const { properties, required } = z.toJSONSchema(
|
|
824
934
|
CHAIN_TX_CATALOG[type].schema,
|
|
935
|
+
{
|
|
936
|
+
override: ({ zodSchema, jsonSchema }) => {
|
|
937
|
+
if (zodSchema !== GROUP_ACTION_SCHEMA) return;
|
|
938
|
+
for (const key of Object.keys(jsonSchema))
|
|
939
|
+
Reflect.deleteProperty(jsonSchema, key);
|
|
940
|
+
Object.assign(jsonSchema, GROUP_ACTION_FIELDS);
|
|
941
|
+
},
|
|
942
|
+
},
|
|
825
943
|
);
|
|
826
944
|
fields = { properties, required };
|
|
827
945
|
catalogueFields.set(type, fields);
|
|
@@ -914,6 +1032,45 @@ export function modelOutcome(
|
|
|
914
1032
|
export const CANCELLED_MEANING =
|
|
915
1033
|
'Withdrawn before anything was sent: nothing happened on chain. The person may have cancelled it, or Auth Hub withdrew it because something it depended on changed after the review; do not say the person cancelled it. If they still want it, offer to prepare a fresh one.';
|
|
916
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* A carried-out group proposal whose actions failed: the transaction
|
|
1037
|
+
* succeeds (code 0) even then, and the network marks the proposal failed
|
|
1038
|
+
* for good, so the receipt's `groupActions` is the only sign.
|
|
1039
|
+
*/
|
|
1040
|
+
export const GROUP_ACTIONS_FAILED_MEANING =
|
|
1041
|
+
'The transaction went through, but the proposal’s actions failed: nothing in the proposal happened, the network has marked it failed for good and it can’t be carried out again. The network fee was used unless a fee grant covered it. Never say it is done; tell the person plainly.';
|
|
1042
|
+
|
|
1043
|
+
/** A carried-out group proposal whose transaction did not show whether its actions ran. */
|
|
1044
|
+
export const GROUP_ACTIONS_UNKNOWN_MEANING =
|
|
1045
|
+
'The transaction went through, but Auth Hub could not tell from it whether the proposal’s actions happened. Check the proposal’s status with chain_lookup dao_proposals (executed: they happened; execution_failed: they did not) before saying anything about it; never say it is done until then.';
|
|
1046
|
+
|
|
1047
|
+
/** A new group proposal whose number Auth Hub could not read from the transaction. */
|
|
1048
|
+
export const PROPOSAL_NUMBER_UNKNOWN_MEANING =
|
|
1049
|
+
'The new proposal was made, but Auth Hub could not read its number from the transaction. Find the new proposal with chain_lookup dao_proposals (the group’s newest) before naming it; never guess its number.';
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* What an outcome means beyond its fields, for the model: a withdrawn card,
|
|
1053
|
+
* a carried-out proposal whose actions failed or may have, a new proposal
|
|
1054
|
+
* without its number. Undefined for anything else.
|
|
1055
|
+
*/
|
|
1056
|
+
export function outcomeMeaning(
|
|
1057
|
+
outcome: ActionOutcomeV1 | null,
|
|
1058
|
+
): string | undefined {
|
|
1059
|
+
if (!outcome) return undefined;
|
|
1060
|
+
if (outcome.status === 'cancelled') return CANCELLED_MEANING;
|
|
1061
|
+
const receipt = outcome.receipt;
|
|
1062
|
+
if (receipt?.kind !== 'ixo.chain.tx.v1') return undefined;
|
|
1063
|
+
for (const step of receipt.steps) {
|
|
1064
|
+
if (step.type === 'daodao.proposal.execute') {
|
|
1065
|
+
if (step.groupActions === 'failed') return GROUP_ACTIONS_FAILED_MEANING;
|
|
1066
|
+
if (step.groupActions === 'unknown') return GROUP_ACTIONS_UNKNOWN_MEANING;
|
|
1067
|
+
}
|
|
1068
|
+
if (step.type === 'daodao.proposal.create' && step.proposalId === null)
|
|
1069
|
+
return PROPOSAL_NUMBER_UNKNOWN_MEANING;
|
|
1070
|
+
}
|
|
1071
|
+
return undefined;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
917
1074
|
async function hubActionStatus(
|
|
918
1075
|
input: unknown,
|
|
919
1076
|
ctx: RuntimeContext,
|
|
@@ -935,15 +1092,14 @@ async function hubActionStatus(
|
|
|
935
1092
|
status: 'unavailable',
|
|
936
1093
|
reason: 'No Hub action with this id was prepared for this owner.',
|
|
937
1094
|
};
|
|
1095
|
+
const meaning = outcomeMeaning(read.outcome);
|
|
938
1096
|
return {
|
|
939
1097
|
status: 'known',
|
|
940
1098
|
reviewStatus: read.reviewStatus,
|
|
941
1099
|
operationStatus: read.operationStatus,
|
|
942
1100
|
outcome: modelOutcome(read.outcome),
|
|
943
1101
|
...(read.summary ? { summary: read.summary } : {}),
|
|
944
|
-
...(
|
|
945
|
-
? { meaning: CANCELLED_MEANING }
|
|
946
|
-
: {}),
|
|
1102
|
+
...(meaning ? { meaning } : {}),
|
|
947
1103
|
};
|
|
948
1104
|
} catch (error) {
|
|
949
1105
|
// An Auth Hub that must be updated first: plain words, not a tool error.
|
|
@@ -1012,6 +1168,7 @@ const manifest: PluginManifest = {
|
|
|
1012
1168
|
'The user asks to file or evaluate a claim, or to send coins or impact tokens on chain.',
|
|
1013
1169
|
'The user asks to stake, unstake or move IXO, claim staking rewards, vote on a network proposal or retire impact tokens.',
|
|
1014
1170
|
'The user asks to reserve or dispute a claim, create or change a claim collection, give or remove a permission, create or update an entity, or change the links of an identity.',
|
|
1171
|
+
'The user asks to vote on a proposal of a group (DAO) they belong to, carry out or close a group proposal, or put a new proposal to a group.',
|
|
1015
1172
|
'The user asks to review an exact private resource or to sign a statement.',
|
|
1016
1173
|
'The user asks to connect Gmail or Calendar, send a reviewed email or add an event.',
|
|
1017
1174
|
'The user asks for document access or to save a private note.',
|
|
@@ -1043,7 +1200,7 @@ export class HubActionsPlugin extends OraclePlugin {
|
|
|
1043
1200
|
/**
|
|
1044
1201
|
* request_chain_transaction is built per turn: its schema names only the
|
|
1045
1202
|
* message types this account may use (Auth Hub capabilities, cached by
|
|
1046
|
-
* the owner object), so
|
|
1203
|
+
* the owner object), so 37 types do not reach a model that may use 4.
|
|
1047
1204
|
*/
|
|
1048
1205
|
override async getRequestTools(ctx: RuntimeContext): Promise<PluginTool[]> {
|
|
1049
1206
|
const [available, described] = await Promise.all([
|
|
@@ -1093,7 +1250,7 @@ export class HubActionsPlugin extends OraclePlugin {
|
|
|
1093
1250
|
{
|
|
1094
1251
|
name: 'hub_action_status',
|
|
1095
1252
|
description:
|
|
1096
|
-
'Read the verified status of a Hub action already prepared for this owner. Never executes. A `cancelled` outcome was withdrawn before anything was sent, by the person or by Auth Hub (`meaning` says so): never say the person cancelled it.',
|
|
1253
|
+
'Read the verified status of a Hub action already prepared for this owner. Never executes. A `cancelled` outcome was withdrawn before anything was sent, by the person or by Auth Hub (`meaning` says so): never say the person cancelled it. When `meaning` is present, it says what the outcome means for the person (for example a carried-out group proposal whose actions failed): follow it.',
|
|
1097
1254
|
schema: statusSchema,
|
|
1098
1255
|
effect: 'read',
|
|
1099
1256
|
handler: hubActionStatus,
|
|
@@ -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,8 +275,10 @@ 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
|
-
'Titles are written by
|
|
281
|
+
'Titles are written by whoever proposed them and group names by the group itself: their own words. Say so only when it matters, in one short, calm note.',
|
|
247
282
|
].join(' '),
|
|
248
283
|
source: 'blocksync',
|
|
249
284
|
list: true,
|