@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
package/src/plugin-api/types.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
ActionReferenceV1,
|
|
30
30
|
ActionOutcomeV1,
|
|
31
31
|
ActionCapabilitiesV1,
|
|
32
|
+
ChainTxGroupAction,
|
|
32
33
|
ChainTxMessage,
|
|
33
34
|
} from '@ixo/action-protocol';
|
|
34
35
|
|
|
@@ -58,32 +59,65 @@ export type ChainTxStakeType =
|
|
|
58
59
|
| 'cosmos.staking.undelegate'
|
|
59
60
|
| 'cosmos.staking.redelegate';
|
|
60
61
|
|
|
62
|
+
/** A bank step as the model writes it: a display amount of a listed token. */
|
|
63
|
+
export type ChainTxBankInput = {
|
|
64
|
+
type: 'cosmos.bank.send';
|
|
65
|
+
toAddress: string;
|
|
66
|
+
/** A plain decimal in the token's display units ("1.5"). */
|
|
67
|
+
amount: string;
|
|
68
|
+
denom?: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** A stake step as the model writes it: a display amount of IXO. */
|
|
72
|
+
export type ChainTxStakeInput = DistributiveOmit<
|
|
73
|
+
Extract<ChainTxMessage, { type: ChainTxStakeType }>,
|
|
74
|
+
'amountBaseUnits'
|
|
75
|
+
> & {
|
|
76
|
+
/** A plain decimal in IXO ("100" or "0.5"). */
|
|
77
|
+
amount: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One action of a new group proposal (`daodao.proposal.create` `actions`)
|
|
82
|
+
* as the model writes it: the protocol's action, with a bank or stake
|
|
83
|
+
* action's amount in display units as at top level. The group's own
|
|
84
|
+
* account is its actor, so none of them names the owner.
|
|
85
|
+
*/
|
|
86
|
+
export type ChainTxGroupActionInput =
|
|
87
|
+
| ChainTxBankInput
|
|
88
|
+
| ChainTxStakeInput
|
|
89
|
+
| Exclude<
|
|
90
|
+
ChainTxGroupAction,
|
|
91
|
+
{ type: 'cosmos.bank.send' | ChainTxStakeType }
|
|
92
|
+
>;
|
|
93
|
+
|
|
61
94
|
/**
|
|
62
95
|
* A chain-transaction message as the model writes it: the protocol's
|
|
63
96
|
* message without the owner's identity (`ChainTxOwnerField`), which the
|
|
64
97
|
* host fills. A bank step names a display `amount` of a token from the
|
|
65
98
|
* owner's list (`denom`, absent = uixo); a stake step a display `amount` of
|
|
66
99
|
* IXO. The host converts both to the protocol's `amountBaseUnits` (the
|
|
67
|
-
* listed decimals; 6 for IXO)
|
|
68
|
-
*
|
|
100
|
+
* listed decimals; 6 for IXO), at top level and inside a new group
|
|
101
|
+
* proposal's actions. Every other amount (impact tokens, a collection's
|
|
102
|
+
* payments) is already in the protocol's units.
|
|
69
103
|
*/
|
|
70
104
|
export type ChainTxMessageInput =
|
|
71
|
-
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
| (DistributiveOmit<
|
|
79
|
-
Extract<ChainTxMessage, { type: ChainTxStakeType }>,
|
|
80
|
-
'amountBaseUnits'
|
|
81
|
-
> & {
|
|
82
|
-
/** A plain decimal in IXO ("100" or "0.5"). */
|
|
83
|
-
amount: string;
|
|
84
|
-
})
|
|
105
|
+
| ChainTxBankInput
|
|
106
|
+
| ChainTxStakeInput
|
|
107
|
+
| (Omit<
|
|
108
|
+
Extract<ChainTxMessage, { type: 'daodao.proposal.create' }>,
|
|
109
|
+
'actions'
|
|
110
|
+
> & { actions: ChainTxGroupActionInput[] })
|
|
85
111
|
| DistributiveOmit<
|
|
86
|
-
Exclude<
|
|
112
|
+
Exclude<
|
|
113
|
+
ChainTxMessage,
|
|
114
|
+
{
|
|
115
|
+
type:
|
|
116
|
+
| 'cosmos.bank.send'
|
|
117
|
+
| ChainTxStakeType
|
|
118
|
+
| 'daodao.proposal.create';
|
|
119
|
+
}
|
|
120
|
+
>,
|
|
87
121
|
ChainTxOwnerField
|
|
88
122
|
>;
|
|
89
123
|
|
|
@@ -869,7 +903,10 @@ export interface RuntimeContext<TConfig = MergedConfig> {
|
|
|
869
903
|
* digits than its decimals, zero or more than the listed balance. A
|
|
870
904
|
* stake step's IXO `amount` is converted with 6 decimals; what a stake
|
|
871
905
|
* takes from the wallet counts against the listed IXO balance with the
|
|
872
|
-
* IXO bank steps, when the list has IXO.
|
|
906
|
+
* IXO bank steps, when the list has IXO. A new group proposal's bank and
|
|
907
|
+
* stake actions are converted the same way but never checked against
|
|
908
|
+
* the owner's balance: the group's own account pays them when the group
|
|
909
|
+
* carries the proposal out.
|
|
873
910
|
*/
|
|
874
911
|
chainTransaction(input: {
|
|
875
912
|
ownerDid: string;
|
|
@@ -3,7 +3,10 @@ import {
|
|
|
3
3
|
CHAIN_TX_COLLECTION_STATES,
|
|
4
4
|
CHAIN_TX_DISPUTE_REASONS,
|
|
5
5
|
CHAIN_TX_ENTITY_PERMISSIONS,
|
|
6
|
+
CHAIN_TX_GROUP_ACTION_TYPES,
|
|
7
|
+
CHAIN_TX_GROUP_VOTE_OPTIONS,
|
|
6
8
|
CHAIN_TX_MAX_CLAIM_PERMISSION,
|
|
9
|
+
CHAIN_TX_MAX_GROUP_ACTIONS,
|
|
7
10
|
CHAIN_TX_MAX_MESSAGES,
|
|
8
11
|
CHAIN_TX_MAX_PAYMENT_COINS,
|
|
9
12
|
CHAIN_TX_MAX_SECONDS,
|
|
@@ -12,6 +15,8 @@ import {
|
|
|
12
15
|
CHAIN_TX_VOTE_OPTIONS,
|
|
13
16
|
DENOM,
|
|
14
17
|
sanitizePublishedText,
|
|
18
|
+
type ChainTxGroupActionType,
|
|
19
|
+
type ChainTxGroupMessageType,
|
|
15
20
|
type ChainTxMessageType,
|
|
16
21
|
} from '@ixo/action-protocol';
|
|
17
22
|
import { z } from 'zod';
|
|
@@ -98,6 +103,20 @@ const SERVICE_REF =
|
|
|
98
103
|
*/
|
|
99
104
|
const LINKED_ENTITY_ADDRESS =
|
|
100
105
|
/^(?:\{id\}#)?ixo1(?:[023456789acdefghjklmnpqrstuvwxyz]{38}|[023456789acdefghjklmnpqrstuvwxyz]{58})$/;
|
|
106
|
+
/**
|
|
107
|
+
* Who receives coins or impact tokens: a 20-byte account or a 32-byte
|
|
108
|
+
* contract address (a group's own account, its treasury). Never a signer or
|
|
109
|
+
* identity field.
|
|
110
|
+
*/
|
|
111
|
+
const RECIPIENT_ADDRESS =
|
|
112
|
+
/^ixo1(?:[023456789acdefghjklmnpqrstuvwxyz]{38}|[023456789acdefghjklmnpqrstuvwxyz]{58})$/;
|
|
113
|
+
/** A 32-byte contract address (a group and its proposal list): `ixo1` and 58 characters. */
|
|
114
|
+
const CONTRACT_ADDRESS = /^ixo1[023456789acdefghjklmnpqrstuvwxyz]{58}$/;
|
|
115
|
+
/**
|
|
116
|
+
* A group proposal's number: digits without leading zeros, at most
|
|
117
|
+
* 2^53 − 1 (Auth Hub writes it as a JSON number).
|
|
118
|
+
*/
|
|
119
|
+
const GROUP_PROPOSAL_ID = /^[1-9][0-9]{0,15}$/;
|
|
101
120
|
|
|
102
121
|
/**
|
|
103
122
|
* Characters that would let text render as something else: C0 and C1
|
|
@@ -181,6 +200,14 @@ const address = (what: string) =>
|
|
|
181
200
|
'A 20-byte ixo1… account address (42 characters); contract addresses are refused.',
|
|
182
201
|
)
|
|
183
202
|
.describe(`${what} (an ixo1… account address).`);
|
|
203
|
+
const recipient = (what: string) =>
|
|
204
|
+
z
|
|
205
|
+
.string()
|
|
206
|
+
.regex(
|
|
207
|
+
RECIPIENT_ADDRESS,
|
|
208
|
+
'An ixo1… account address (42 characters), or a group’s own address (62 characters, `dao` in chain_lookup daos or dao_proposals), copied exactly.',
|
|
209
|
+
)
|
|
210
|
+
.describe(`${what} (an ixo1… account address, or a group’s own address).`);
|
|
184
211
|
const validator = (what: string) =>
|
|
185
212
|
z
|
|
186
213
|
.string()
|
|
@@ -494,11 +521,12 @@ const IID_REMOVE_RULE =
|
|
|
494
521
|
'Copy the id exactly as the lookup shows it, whatever its form (`#api`, `{id}#ixo1…`): Auth Hub checks it is there before it makes the card.';
|
|
495
522
|
|
|
496
523
|
/**
|
|
497
|
-
* The
|
|
498
|
-
*
|
|
499
|
-
*
|
|
524
|
+
* The types the owner's own account does (every type but the four group
|
|
525
|
+
* steps). Every `rules` text ends with how to say the step to the owner:
|
|
526
|
+
* plain everyday words, never the message type or chain terms (the review
|
|
527
|
+
* card in Auth Hub shows the exact values).
|
|
500
528
|
*/
|
|
501
|
-
|
|
529
|
+
const OWNER_CATALOG = {
|
|
502
530
|
'cosmos.bank.send': {
|
|
503
531
|
summary:
|
|
504
532
|
"send coins (IXO or another token the owner holds) from the owner's wallet.",
|
|
@@ -506,11 +534,12 @@ export const CHAIN_TX_CATALOG = {
|
|
|
506
534
|
"Sends coins from the owner's wallet to `toAddress`, alone (a single coin send) or as a step of a batch.",
|
|
507
535
|
'`amount` is the amount as the user said it, in the token’s display units ("1.5" for 1.5 IXO): a plain decimal, never base units. The runtime looks the token up in the owner’s token list (hub_action_options `transfer.tokens`), converts the amount with the listed `decimals`, and refuses a token not in the list, more digits after the point than the token has, zero, and more than the listed balance (the bank and stake steps of one token together).',
|
|
508
536
|
'`denom` is optional and defaults to uixo (IXO, 6 decimals). For another token copy its `denom` exactly from `transfer.tokens` (never guess an `ibc/…` hash); name the full denom to the owner for anything but IXO, and say when the token is unverified (it lists 0 decimals: its amount is a whole number of base units).',
|
|
509
|
-
'
|
|
537
|
+
'`toAddress` may also be a group’s own address (62 characters): coins sent there go to the group’s treasury. Find it with chain_lookup daos or dao_proposals (`dao`) when the owner names the group, and say the group’s name.',
|
|
538
|
+
'Say it like: “Send 1.5 IXO to Thandi (ixo1qy…lvf0).” or “Send 2 IXO to the Auth Hub drill group.”',
|
|
510
539
|
].join(' '),
|
|
511
540
|
schema: z.strictObject({
|
|
512
541
|
type: z.literal('cosmos.bank.send'),
|
|
513
|
-
toAddress:
|
|
542
|
+
toAddress: recipient('The recipient'),
|
|
514
543
|
amount: z
|
|
515
544
|
.string()
|
|
516
545
|
.max(80)
|
|
@@ -583,10 +612,10 @@ export const CHAIN_TX_CATALOG = {
|
|
|
583
612
|
'ixo.token.transfer': {
|
|
584
613
|
summary:
|
|
585
614
|
"move IXO impact tokens (1155-style token ids, whole amounts) from the owner's wallet.",
|
|
586
|
-
rules: `Moves IXO impact tokens (1155-style: each line a token \`id\` of 32 hex characters and a whole \`amount\`, 1 to ${CHAIN_TX_MAX_TOKEN_LINES} lines) from the owner’s wallet to \`recipient
|
|
615
|
+
rules: `Moves IXO impact tokens (1155-style: each line a token \`id\` of 32 hex characters and a whole \`amount\`, 1 to ${CHAIN_TX_MAX_TOKEN_LINES} lines) from the owner’s wallet to \`recipient\` (an account, or a group’s own address from chain_lookup daos or dao_proposals). Impact tokens have no decimals: an amount is a whole number of tokens. All tokens in one message must come from one token contract. These are not the IXO coin: coins go through \`cosmos.bank.send\`. Say it like: “Give 3 of your carbon credits to Thandi (ixo1qy…lvf0).”`,
|
|
587
616
|
schema: z.strictObject({
|
|
588
617
|
type: z.literal('ixo.token.transfer'),
|
|
589
|
-
recipient:
|
|
618
|
+
recipient: recipient('The recipient'),
|
|
590
619
|
tokens: z
|
|
591
620
|
.array(
|
|
592
621
|
z.strictObject({
|
|
@@ -1158,14 +1187,213 @@ export const CHAIN_TX_CATALOG = {
|
|
|
1158
1187
|
),
|
|
1159
1188
|
}),
|
|
1160
1189
|
},
|
|
1190
|
+
} as const satisfies Record<
|
|
1191
|
+
Exclude<ChainTxMessageType, ChainTxGroupMessageType>,
|
|
1192
|
+
ChainTxCatalogEntry
|
|
1193
|
+
>;
|
|
1194
|
+
|
|
1195
|
+
// ---- Groups (DAO DAO groups with single-choice proposals) ----
|
|
1196
|
+
|
|
1197
|
+
const contractAddress = (what: string) =>
|
|
1198
|
+
z
|
|
1199
|
+
.string()
|
|
1200
|
+
.regex(
|
|
1201
|
+
CONTRACT_ADDRESS,
|
|
1202
|
+
'A 32-byte ixo1… contract address (62 characters), copied exactly from chain_lookup daos or dao_proposals.',
|
|
1203
|
+
)
|
|
1204
|
+
.describe(what);
|
|
1205
|
+
const groupDao = contractAddress(
|
|
1206
|
+
'The group: its own address (`dao` in chain_lookup daos or dao_proposals).',
|
|
1207
|
+
);
|
|
1208
|
+
const proposalModule = contractAddress(
|
|
1209
|
+
'The group’s proposal list the step acts on (`proposalModule` in chain_lookup dao_proposals, or a `proposalLists` entry in daos); never the group’s own address.',
|
|
1210
|
+
);
|
|
1211
|
+
const groupProposalId = z
|
|
1212
|
+
.string()
|
|
1213
|
+
.regex(
|
|
1214
|
+
GROUP_PROPOSAL_ID,
|
|
1215
|
+
'The proposal number: digits from 1, no leading zeros (the `id`, not the label such as A3).',
|
|
1216
|
+
)
|
|
1217
|
+
// zod runs every check: a value the pattern refused is not parsed here.
|
|
1218
|
+
.refine(
|
|
1219
|
+
(value) =>
|
|
1220
|
+
!GROUP_PROPOSAL_ID.test(value) ||
|
|
1221
|
+
BigInt(value) <= BigInt(Number.MAX_SAFE_INTEGER),
|
|
1222
|
+
{ message: `At most ${Number.MAX_SAFE_INTEGER}.` },
|
|
1223
|
+
)
|
|
1224
|
+
.describe(
|
|
1225
|
+
'The proposal’s number in its proposal list (`id` in chain_lookup dao_proposals, not the label).',
|
|
1226
|
+
);
|
|
1227
|
+
/** Text published on chain with a group step: plain and shown exactly as written. */
|
|
1228
|
+
const groupText = (max: number, what: string) =>
|
|
1229
|
+
plainText(max, what).refine(shownAsIs, { message: SHOWN_AS_IS });
|
|
1230
|
+
/** `dao` and `proposalModule` are two different contracts (the protocol names `proposalModule`). */
|
|
1231
|
+
function twoContracts(
|
|
1232
|
+
message: { dao: string; proposalModule: string },
|
|
1233
|
+
ctx: z.RefinementCtx,
|
|
1234
|
+
): void {
|
|
1235
|
+
if (message.proposalModule === message.dao)
|
|
1236
|
+
ctx.addIssue({
|
|
1237
|
+
code: 'custom',
|
|
1238
|
+
path: ['proposalModule'],
|
|
1239
|
+
message:
|
|
1240
|
+
'The proposal list, a different address than the group’s own (`dao`).',
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* What a group proposal's action may be, by type: exactly the top-level
|
|
1246
|
+
* message of that type (same fields, same rules), with the group's own
|
|
1247
|
+
* account as the actor. A dates change names both dates: a date left out
|
|
1248
|
+
* would be read now and be out of date when the group carries it out.
|
|
1249
|
+
*/
|
|
1250
|
+
const GROUP_ACTION_SCHEMAS = {
|
|
1251
|
+
'cosmos.bank.send': OWNER_CATALOG['cosmos.bank.send'].schema,
|
|
1252
|
+
'ixo.token.transfer': OWNER_CATALOG['ixo.token.transfer'].schema,
|
|
1253
|
+
'ixo.token.retire': OWNER_CATALOG['ixo.token.retire'].schema,
|
|
1254
|
+
'cosmos.staking.delegate': OWNER_CATALOG['cosmos.staking.delegate'].schema,
|
|
1255
|
+
'cosmos.staking.undelegate':
|
|
1256
|
+
OWNER_CATALOG['cosmos.staking.undelegate'].schema,
|
|
1257
|
+
'cosmos.staking.redelegate':
|
|
1258
|
+
OWNER_CATALOG['cosmos.staking.redelegate'].schema,
|
|
1259
|
+
'cosmos.distribution.withdraw-rewards':
|
|
1260
|
+
OWNER_CATALOG['cosmos.distribution.withdraw-rewards'].schema,
|
|
1261
|
+
'cosmos.gov.vote': OWNER_CATALOG['cosmos.gov.vote'].schema,
|
|
1262
|
+
'ixo.claims.update-collection-state':
|
|
1263
|
+
OWNER_CATALOG['ixo.claims.update-collection-state'].schema,
|
|
1264
|
+
'ixo.claims.update-collection-dates': z
|
|
1265
|
+
.strictObject({
|
|
1266
|
+
type: z.literal('ixo.claims.update-collection-dates'),
|
|
1267
|
+
collectionId,
|
|
1268
|
+
adminAddress,
|
|
1269
|
+
startDate: instant('When it opens (always given in a group proposal).'),
|
|
1270
|
+
endDate: instant(
|
|
1271
|
+
'When it closes; null: no end date (always given in a group proposal).',
|
|
1272
|
+
).nullable(),
|
|
1273
|
+
})
|
|
1274
|
+
.superRefine(endAfterStart),
|
|
1275
|
+
'ixo.claims.update-collection-quota':
|
|
1276
|
+
OWNER_CATALOG['ixo.claims.update-collection-quota'].schema,
|
|
1277
|
+
'ixo.claims.create-collection':
|
|
1278
|
+
OWNER_CATALOG['ixo.claims.create-collection'].schema,
|
|
1279
|
+
'ixo.entity.create-account':
|
|
1280
|
+
OWNER_CATALOG['ixo.entity.create-account'].schema,
|
|
1281
|
+
'ixo.entity.revoke-account-authz':
|
|
1282
|
+
OWNER_CATALOG['ixo.entity.revoke-account-authz'].schema,
|
|
1283
|
+
} as const satisfies Record<ChainTxGroupActionType, z.ZodObject>;
|
|
1284
|
+
|
|
1285
|
+
/** The action types a group proposal may hold, in the protocol's order. */
|
|
1286
|
+
export const GROUP_ACTION_TYPES: readonly ChainTxGroupActionType[] =
|
|
1287
|
+
CHAIN_TX_GROUP_ACTION_TYPES;
|
|
1288
|
+
|
|
1289
|
+
const [firstGroupAction, ...otherGroupActions] = GROUP_ACTION_TYPES.map(
|
|
1290
|
+
(type) => GROUP_ACTION_SCHEMAS[type],
|
|
1291
|
+
);
|
|
1292
|
+
if (!firstGroupAction)
|
|
1293
|
+
throw new Error('The protocol lists no group action types');
|
|
1294
|
+
/**
|
|
1295
|
+
* One action of a new group proposal: its `type` picks the schema. A type a
|
|
1296
|
+
* group cannot do is refused on `actions[j].type`, as the protocol does.
|
|
1297
|
+
*/
|
|
1298
|
+
export const GROUP_ACTION_SCHEMA = z.discriminatedUnion(
|
|
1299
|
+
'type',
|
|
1300
|
+
[firstGroupAction, ...otherGroupActions],
|
|
1301
|
+
{
|
|
1302
|
+
error: `A group proposal can only hold these actions: ${GROUP_ACTION_TYPES.join(', ')}.`,
|
|
1303
|
+
},
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
/** Said once for the four group steps. */
|
|
1307
|
+
const GROUP_RULES =
|
|
1308
|
+
'A group here is a DAO DAO group on the IXO network (people say group, club, cooperative or DAO). Find it and its proposals with chain_lookup dao_proposals (`dao`, `proposalModule`, `id`, `label`, title, status, when voting closes) or chain_lookup daos (the person’s groups and, per group, `proposalLists` for new proposals). Copy `dao`, `proposalModule` and `proposalId` exactly from there: `dao` is the group’s own address, `proposalModule` its proposal list (a different address), `proposalId` the number without the list’s letter. Auth Hub checks the group and its proposal on chain and refuses what the group does not allow. One group step per card: nothing else goes on the same card. Only single-choice proposals are supported (not multiple choice). The review card may ask the owner to confirm twice: in the groups in use today one admin account can change how the group works without a vote, and the card says so. If the owner asks why, explain that calmly in one sentence; it is how these groups are set up, not a problem with this card.';
|
|
1309
|
+
|
|
1310
|
+
const GROUP_CATALOG = {
|
|
1311
|
+
'daodao.proposal.vote': {
|
|
1312
|
+
summary:
|
|
1313
|
+
'vote yes, no or abstain on a proposal in a group (DAO) the owner belongs to.',
|
|
1314
|
+
rules: `${GROUP_RULES} Records the owner’s vote on one group proposal while its voting is open (\`votingEnded\` absent in dao_proposals). Before asking how they vote, tell the owner the proposal’s title and label and when voting closes, and whether they can change the vote later (\`allowRevoting\`); never vote without the owner saying how, in this conversation. \`vote\`: \`yes\`, \`no\` or \`abstain\`. \`rationale\` is optional: the owner’s reason, published with the vote, up to 300 characters, exactly as they said it (no links, no hidden characters, one line). The weight of the vote is what the owner had in the group when the proposal started: Auth Hub refuses the card when they had none, and when they already voted on a proposal that doesn’t allow changing a vote. A vote on a proposal whose result is already decided is still recorded until voting closes, but can’t change the result. Say it like: “Vote Yes on proposal A3, ‘Fix the borehole’, in Clean Water Group. This group doesn’t let you change your vote later.”`,
|
|
1315
|
+
schema: z
|
|
1316
|
+
.strictObject({
|
|
1317
|
+
type: z.literal('daodao.proposal.vote'),
|
|
1318
|
+
dao: groupDao,
|
|
1319
|
+
proposalModule,
|
|
1320
|
+
proposalId: groupProposalId,
|
|
1321
|
+
vote: z
|
|
1322
|
+
.literal([...CHAIN_TX_GROUP_VOTE_OPTIONS])
|
|
1323
|
+
.describe('yes, no or abstain.'),
|
|
1324
|
+
rationale: groupText(
|
|
1325
|
+
300,
|
|
1326
|
+
'Optional: the owner’s reason, published with the vote.',
|
|
1327
|
+
).optional(),
|
|
1328
|
+
})
|
|
1329
|
+
.superRefine(twoContracts),
|
|
1330
|
+
},
|
|
1331
|
+
'daodao.proposal.execute': {
|
|
1332
|
+
summary:
|
|
1333
|
+
'carry out a group proposal that passed, so the group’s own account does what it says.',
|
|
1334
|
+
rules: `${GROUP_RULES} Carries out a proposal that passed (\`canRun\` in dao_proposals): the group’s own account then does the proposal’s actions, which the review card lists. A proposal with no actions (one that asked for agreement only) is simply marked as carried out: never describe an action for it; say “This just marks it as carried out; nothing else happens.” On a proposal list the group switched off (\`listDisabled\`), a proposal with actions can’t be carried out. Only when the owner asks for it. Tell them what the proposal does (its title) before preparing. Some groups let only members carry proposals out. Auth Hub tries it first and refuses the card when the actions would fail now (in most groups a failed run marks the proposal failed for good), for example when the group doesn’t hold enough; tell the owner it can be tried again once that is fixed. It costs the owner only the network fee; a deposit paid with the proposal goes back to whoever proposed it unless the group keeps deposits (the review card says). After it goes through (hub_action_status), read the receipt’s \`groupActions\`: \`"failed"\` means the proposal’s actions did not happen and it can’t be tried again; never say it is done. \`"unknown"\`: check its status with chain_lookup dao_proposals before saying anything. Say it like: “Carry out proposal A3, ‘Pay Thandi’, in Clean Water Group: the group sends Thandi 500 IXO. This can’t be undone.”`,
|
|
1335
|
+
schema: z
|
|
1336
|
+
.strictObject({
|
|
1337
|
+
type: z.literal('daodao.proposal.execute'),
|
|
1338
|
+
dao: groupDao,
|
|
1339
|
+
proposalModule,
|
|
1340
|
+
proposalId: groupProposalId,
|
|
1341
|
+
})
|
|
1342
|
+
.superRefine(twoContracts),
|
|
1343
|
+
},
|
|
1344
|
+
'daodao.proposal.close': {
|
|
1345
|
+
summary:
|
|
1346
|
+
'close a group proposal that did not pass (its actions will never happen).',
|
|
1347
|
+
rules: `${GROUP_RULES} Closes a proposal that did not pass (\`canClose\` in dao_proposals): none of its actions will happen. Only when the owner asks for it. Closing does not give its deposit back to whoever proposed it: in most groups the deposit goes to the group (the review card says where it goes); say so. Say it like: “Close proposal A3 in Clean Water Group. It didn’t pass, so nothing in it will happen; its 1 IXO deposit goes to the group, not back to whoever proposed it.”`,
|
|
1348
|
+
schema: z
|
|
1349
|
+
.strictObject({
|
|
1350
|
+
type: z.literal('daodao.proposal.close'),
|
|
1351
|
+
dao: groupDao,
|
|
1352
|
+
proposalModule,
|
|
1353
|
+
proposalId: groupProposalId,
|
|
1354
|
+
})
|
|
1355
|
+
.superRefine(twoContracts),
|
|
1356
|
+
},
|
|
1357
|
+
'daodao.proposal.create': {
|
|
1358
|
+
summary:
|
|
1359
|
+
'ask a group (DAO) to vote on a new proposal: agreement only, or up to 4 things the group’s own account then does.',
|
|
1360
|
+
rules: `${GROUP_RULES} Asks the group to vote on a new proposal from the owner. Never propose without the owner asking for it and agreeing to the exact title, description and actions. \`proposalModule\` is one of the group’s \`proposalLists\` in chain_lookup daos (use one that is not \`disabled\` and whose \`deposit\` a card can pay; \`membersOnly: true\`: only members may propose). \`title\` (up to 100 characters) and \`description\` (up to 300) are published as written: plain text on one line, no links, no hidden characters. \`actions\` is what the group’s own account does if the proposal passes: \`[]\` asks for agreement only; otherwise up to ${CHAIN_TX_MAX_GROUP_ACTIONS} actions, each written exactly like a top-level message of its type (describe that type too), of these types only: ${GROUP_ACTION_TYPES.join(', ')}. In an action the group is the actor, not the owner: a coin send goes from the group’s own account (\`dao\`; check what it holds with chain_lookup balances for that address), a stake stakes the group’s IXO, a collection change needs a permission given to the group’s address, a new collection or an entity account needs an entity the group owns, and a collection dates change gives both \`startDate\` and \`endDate\` (null = no end). Amounts are display units as at top level (IXO by default; another token only one listed in hub_action_options \`transfer.tokens\`, for its decimals); the owner’s own balance is not used. The network checks each action only when the group carries the proposal out, possibly days later; if one fails then, none happen. When the group takes a deposit, it comes from the owner’s wallet: say how much, and that it may be lost if the proposal doesn’t pass or is never carried out (it comes back only as the group’s refund rule says, \`refundPolicy\` in daos). A proposal can’t be withdrawn once made. Once it went through (hub_action_status), the receipt names the new proposal’s number (\`proposalId\`); when it is null, find the new proposal with chain_lookup dao_proposals and never guess its number. Say it like: “Propose to Clean Water Group: ‘Pay Thandi for the borehole repair’. If it passes, the group sends her 500 IXO; proposing costs you a 1 IXO deposit, which you get back only if it passes and is carried out.”`,
|
|
1361
|
+
schema: z
|
|
1362
|
+
.strictObject({
|
|
1363
|
+
type: z.literal('daodao.proposal.create'),
|
|
1364
|
+
dao: groupDao,
|
|
1365
|
+
proposalModule,
|
|
1366
|
+
title: groupText(100, 'The proposal’s title, as published.'),
|
|
1367
|
+
description: groupText(
|
|
1368
|
+
300,
|
|
1369
|
+
'What the proposal asks, as published (one line).',
|
|
1370
|
+
),
|
|
1371
|
+
actions: z
|
|
1372
|
+
.array(GROUP_ACTION_SCHEMA)
|
|
1373
|
+
.max(CHAIN_TX_MAX_GROUP_ACTIONS)
|
|
1374
|
+
.describe(
|
|
1375
|
+
`What the group’s own account does if it passes: [] for agreement only, or up to ${CHAIN_TX_MAX_GROUP_ACTIONS} actions, each a message of one of: ${GROUP_ACTION_TYPES.join(', ')}.`,
|
|
1376
|
+
),
|
|
1377
|
+
})
|
|
1378
|
+
.superRefine(twoContracts),
|
|
1379
|
+
},
|
|
1380
|
+
} as const satisfies Record<ChainTxGroupMessageType, ChainTxCatalogEntry>;
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* The catalogue: every message type the protocol knows, the owner's own
|
|
1384
|
+
* types first, then the four group steps (the protocol's order).
|
|
1385
|
+
*/
|
|
1386
|
+
export const CHAIN_TX_CATALOG = {
|
|
1387
|
+
...OWNER_CATALOG,
|
|
1388
|
+
...GROUP_CATALOG,
|
|
1161
1389
|
} as const satisfies Record<ChainTxMessageType, ChainTxCatalogEntry>;
|
|
1162
1390
|
|
|
1163
1391
|
/** What every message type shares, returned once by describe_chain_messages. */
|
|
1164
1392
|
export const CHAIN_TX_GENERAL_RULES = [
|
|
1165
|
-
'Addresses are 20-byte `ixo1…` account addresses (contract addresses
|
|
1393
|
+
'Addresses are 20-byte `ixo1…` account addresses, except where a type says otherwise: a coin send or impact-token transfer may go to a group’s own address, and the group steps take the group’s and its proposal list’s addresses (32-byte `ixo1…` contract addresses, 62 characters); validators `ixovaloper1…`, entities `did:ixo:entity:…`. Copy every id, address and number exactly from a lookup (chain_lookup, hub_action_options) or from the owner; never invent or guess one. Ids (`claimId`, `collectionId`, token `id`, proofs) use only letters, digits and `: . _ / -`.',
|
|
1166
1394
|
'Give only the fields listed for a type: the runtime adds the chain, the network, the signer and the owner’s own DID and address where a message needs them, and Auth Hub sets the fee. Amounts: coin sends and stakes are what the owner says in the token’s units ("100" IXO); impact tokens are whole tokens; collection payments are base units (1 IXO = 1000000 uixo). Dates are RFC 3339 with Z or an offset, from the owner’s time zone.',
|
|
1167
|
-
`Up to ${CHAIN_TX_MAX_MESSAGES} messages per transaction, run in the order given (put them in the order the owner said them, and keep one decision in one card); all succeed or none do. A step that needs something stored first (a dispute statement, a profile, a document) can only be prepared once it is stored.`,
|
|
1168
|
-
'How to talk about it: before preparing, say in one or two plain, everyday sentences what will happen and what it means for the owner (what leaves their wallet, what someone else will be able to do, how long it takes, whether it can be undone), the way each type’s “Say it like” does. Use names (of the validator, collection, entity or person) when a lookup gives them. Never say message type names or chain words such as authz, grant, MsgExec, bech32, valoper, uixo, base units, DID, IID, delegate or
|
|
1395
|
+
`Up to ${CHAIN_TX_MAX_MESSAGES} messages per transaction, run in the order given (put them in the order the owner said them, and keep one decision in one card); all succeed or none do. A group step (a vote, carrying out or closing a group proposal, a new group proposal) goes alone on its card. A step that needs something stored first (a dispute statement, a profile, a document) can only be prepared once it is stored.`,
|
|
1396
|
+
'How to talk about it: before preparing, say in one or two plain, everyday sentences what will happen and what it means for the owner (what leaves their wallet, what someone else will be able to do, how long it takes, whether it can be undone), the way each type’s “Say it like” does. Use names (of the validator, collection, entity or person) when a lookup gives them. Never say message type names or chain words such as authz, grant, MsgExec, bech32, valoper, uixo, base units, DID, IID, delegate, redelegate, DAO, proposal module, contract or execute: say stake, unstake, move stake, permission, account, identity, IXO, group, proposal, vote, deposit and carry out. Show an address only when the owner needs it to check who receives something. The review card in Auth Hub shows every exact value.',
|
|
1169
1397
|
].join(' ');
|
|
1170
1398
|
|
|
1171
1399
|
/** A catalogue message as its schema parses it. */
|