@ixo/oracle-runtime-workers 0.31.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/core/middlewares/tool-repetition-guard.ts +6 -3
- package/src/core/middlewares/turn-boundary.ts +6 -1
- package/src/core/tool-not-run.ts +30 -0
- package/src/core/wrap-plugin-tool.ts +9 -1
- package/src/do/user-oracle-do.ts +78 -17
- package/src/hub-actions/chain-tx-refusals.ts +307 -0
- package/src/hub-actions/client.ts +128 -30
- package/src/hub-actions/request.ts +32 -25
- package/src/hub-actions/transfer.ts +187 -33
- package/src/plugin-api/types.ts +103 -25
- package/src/plugins/hub-actions/chain-tx-catalog.ts +1451 -0
- package/src/plugins/hub-actions/hub-actions.plugin.ts +558 -222
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +1 -1
- package/src/plugins/ixo-chain/kinds/chain/index.ts +2 -0
- package/src/plugins/ixo-chain/kinds/chain/validators.ts +151 -0
- package/src/plugins/ixo-chain/kinds/indexed/dao-proposals.ts +37 -2
- package/src/plugins/ixo-chain/kinds/indexed/daos.ts +145 -9
- package/src/plugins/yoma/engine.ts +107 -55
- package/src/plugins/yoma/submission.ts +3 -6
- package/src/plugins/yoma/yoma.plugin.ts +12 -3
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type ActionOutcomeV1,
|
|
10
10
|
type ActionReferenceV1,
|
|
11
11
|
type ActionReviewView,
|
|
12
|
+
type ChainTxMessageType,
|
|
12
13
|
type HubActionOptionsV2,
|
|
13
14
|
type HubActionOptionsV3,
|
|
14
15
|
} from '@ixo/action-protocol';
|
|
@@ -86,8 +87,18 @@ export interface HubActionTransport {
|
|
|
86
87
|
|
|
87
88
|
/**
|
|
88
89
|
* The protocol version this runtime declares on every operation call
|
|
89
|
-
* (capabilities, POST operations, GET status, options). Version
|
|
90
|
-
* 0.
|
|
90
|
+
* (capabilities, POST operations, GET status, options). Version 7 is the
|
|
91
|
+
* 0.9.0 contract: version 6 plus the four group steps on `ixo.chain.tx.v1`
|
|
92
|
+
* (a vote on a group proposal, carrying out a passed one, closing a failed
|
|
93
|
+
* one, a new proposal), which a 0.8.0 parser cannot read in capabilities,
|
|
94
|
+
* review steps or receipts, so the Hub lists and accepts them only at
|
|
95
|
+
* level 7; options answer as at version 3. Version 6 is the 0.8.0
|
|
96
|
+
* contract: version 5 plus 29 more `ixo.chain.tx.v1` message types
|
|
97
|
+
* (staking, voting, retiring impact tokens, claim collections, entities and
|
|
98
|
+
* identity-record links), which a 0.7.0 parser cannot read in capabilities,
|
|
99
|
+
* review steps or receipts, so the Hub lists and accepts them only at level
|
|
100
|
+
* 6 (`chainTxMessageTypesForLevel`); options answer as at version 3.
|
|
101
|
+
* Version 5 is the 0.7.0 contract: version 4 plus the application card (`ixo.bid.submit.v1`)
|
|
91
102
|
* and the claim card (`ixo.claim.submit.v1`) with their receipts
|
|
92
103
|
* (`ixo.bid.submission.v1`, `ixo.claim.submission.v1`,
|
|
93
104
|
* `ixo.submission.failed.v1`), which a 0.6.0 parser cannot read, so the Hub
|
|
@@ -101,22 +112,38 @@ export interface HubActionTransport {
|
|
|
101
112
|
* the 0.4.1 shapes. The envelopes stay tagged `ixo.auth-outcome/2` and
|
|
102
113
|
* `ixo.auth-capabilities/2`, which `@ixo/action-protocol` parses.
|
|
103
114
|
*
|
|
104
|
-
* An Auth Hub older than version
|
|
105
|
-
* list (1 to
|
|
106
|
-
*
|
|
115
|
+
* An Auth Hub older than this runtime's version checks `?version=` against
|
|
116
|
+
* its closed list (1 to 6 on 0.8.0, 1 to 5 on 0.7.0) and answers every call
|
|
117
|
+
* that declares a version it does not know with 400 `invalid_request`,
|
|
118
|
+
* before anything else. This runtime then steps down
|
|
119
|
+
* (`HUB_PROTOCOL_VERSIONS`: 7, then 6, then 5) to the highest version that
|
|
120
|
+
* Hub answers (every Hub action the lower level has keeps working while
|
|
121
|
+
* Auth Hub is rolled out: the group steps at 6, the level-6 chain types too
|
|
122
|
+
* at 5) and asks for 7 again after `HUB_PROTOCOL_RECHECK_MS`; a Hub that
|
|
123
|
+
* refuses 5 too is told apart with `HUB_UPDATE_REQUIRED`.
|
|
107
124
|
*/
|
|
108
|
-
const HUB_PROTOCOL_VERSION = '
|
|
109
|
-
|
|
125
|
+
const HUB_PROTOCOL_VERSION = '7';
|
|
126
|
+
/** The lowest version this runtime works at: 7 without the group steps and the level-6 chain types. */
|
|
127
|
+
const HUB_PROTOCOL_LOWEST_VERSION = '5';
|
|
128
|
+
/** Every version this runtime speaks, highest first. */
|
|
129
|
+
const HUB_PROTOCOL_VERSIONS = [
|
|
130
|
+
HUB_PROTOCOL_VERSION,
|
|
131
|
+
'6',
|
|
132
|
+
HUB_PROTOCOL_LOWEST_VERSION,
|
|
133
|
+
] as const;
|
|
134
|
+
type HubProtocolVersion = (typeof HUB_PROTOCOL_VERSIONS)[number];
|
|
135
|
+
/** How long a Hub that refused a version is spoken to at a lower one before the highest is tried again. */
|
|
136
|
+
export const HUB_PROTOCOL_RECHECK_MS = 10 * 60_000;
|
|
110
137
|
|
|
111
138
|
/**
|
|
112
139
|
* What the model is told when Auth Hub refuses a read (capabilities,
|
|
113
140
|
* options, status) with 400 `invalid_request`. A read carries nothing but
|
|
114
141
|
* the protocol version (and, for options, the binding the runtime itself
|
|
115
|
-
* checked), so that answer comes from a Hub that does not know version
|
|
116
|
-
* The owner object keeps every action it holds: polls and
|
|
117
|
-
* the outcomes up once the Hub is updated.
|
|
142
|
+
* checked), so that answer comes from a Hub that does not know the version
|
|
143
|
+
* declared. The owner object keeps every action it holds: polls and
|
|
144
|
+
* callbacks pick the outcomes up once the Hub is updated.
|
|
118
145
|
*/
|
|
119
|
-
export const HUB_UPDATE_REQUIRED = `Auth Hub must be updated before this oracle can use Hub actions: it does not accept protocol version ${
|
|
146
|
+
export const HUB_UPDATE_REQUIRED = `Auth Hub must be updated before this oracle can use Hub actions: it does not accept protocol version ${HUB_PROTOCOL_LOWEST_VERSION}, the lowest this runtime works with (invalid_request). Tell the person this is not available right now; retrying or changing the fields will not help.`;
|
|
120
147
|
/** Every Hub call but a chain-transaction prepare. */
|
|
121
148
|
const HUB_TIMEOUT_MS = 10_000;
|
|
122
149
|
/**
|
|
@@ -131,8 +158,19 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
131
158
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
132
159
|
}
|
|
133
160
|
|
|
161
|
+
/** A read Auth Hub refused because it does not know the declared version. */
|
|
162
|
+
function isUpdateRequired(error: unknown): boolean {
|
|
163
|
+
return (
|
|
164
|
+
error instanceof HubActionRefused && error.message === HUB_UPDATE_REQUIRED
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
134
168
|
export class HubActionClient {
|
|
135
169
|
private readonly origin: string;
|
|
170
|
+
/** The lower version this Hub answered after refusing a higher one. */
|
|
171
|
+
private fallbackVersion: HubProtocolVersion = HUB_PROTOCOL_VERSION;
|
|
172
|
+
/** Until when this Hub is spoken to at `fallbackVersion`. */
|
|
173
|
+
private fallbackUntil = 0;
|
|
136
174
|
|
|
137
175
|
constructor(private readonly transport: HubActionTransport) {
|
|
138
176
|
const url = new URL(transport.origin);
|
|
@@ -152,12 +190,56 @@ export class HubActionClient {
|
|
|
152
190
|
this.origin = url.origin;
|
|
153
191
|
}
|
|
154
192
|
|
|
155
|
-
/**
|
|
193
|
+
/** The protocol version the next call declares. */
|
|
194
|
+
get protocolVersion(): HubProtocolVersion {
|
|
195
|
+
return Date.now() < this.fallbackUntil
|
|
196
|
+
? this.fallbackVersion
|
|
197
|
+
: HUB_PROTOCOL_VERSION;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A read at the current version; when Auth Hub refuses it as unknown,
|
|
202
|
+
* the same read at each lower version in turn (`HUB_PROTOCOL_VERSIONS`),
|
|
203
|
+
* and the version it answered is remembered for
|
|
204
|
+
* `HUB_PROTOCOL_RECHECK_MS`. Only reads do this: a refused read changed
|
|
205
|
+
* nothing, while a prepare's 400 can be a real refusal of its operation.
|
|
206
|
+
* A Hub that refuses the lowest version too answers `HUB_UPDATE_REQUIRED`.
|
|
207
|
+
*/
|
|
208
|
+
private async negotiated<T>(
|
|
209
|
+
read: (version: HubProtocolVersion) => Promise<T>,
|
|
210
|
+
): Promise<T> {
|
|
211
|
+
const current = this.protocolVersion;
|
|
212
|
+
const versions = HUB_PROTOCOL_VERSIONS.slice(
|
|
213
|
+
HUB_PROTOCOL_VERSIONS.indexOf(current),
|
|
214
|
+
);
|
|
215
|
+
for (const [step, version] of versions.entries()) {
|
|
216
|
+
try {
|
|
217
|
+
const answer = await read(version);
|
|
218
|
+
if (step > 0) {
|
|
219
|
+
this.fallbackVersion = version;
|
|
220
|
+
this.fallbackUntil = Date.now() + HUB_PROTOCOL_RECHECK_MS;
|
|
221
|
+
}
|
|
222
|
+
return answer;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (step === versions.length - 1 || !isUpdateRequired(error))
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Unreachable: the loop returns or throws at its last version.
|
|
229
|
+
throw new Error('No Hub protocol version to read at');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* `subject`: what a refused prepare is called in the model's words;
|
|
234
|
+
* `steps`: a chain transaction's message types, for the words of a refusal
|
|
235
|
+
* that depends on them.
|
|
236
|
+
*/
|
|
156
237
|
private async send(
|
|
157
238
|
path: string,
|
|
158
239
|
body?: unknown,
|
|
159
240
|
timeoutMs = HUB_TIMEOUT_MS,
|
|
160
241
|
subject?: HubPrepareSubject,
|
|
242
|
+
steps?: readonly ChainTxMessageType[],
|
|
161
243
|
): Promise<unknown> {
|
|
162
244
|
const request = new Request(new URL(path, this.origin), {
|
|
163
245
|
method: body === undefined ? 'GET' : 'POST',
|
|
@@ -191,6 +273,7 @@ export class HubActionClient {
|
|
|
191
273
|
response.status,
|
|
192
274
|
await response.json().catch(() => null),
|
|
193
275
|
subject,
|
|
276
|
+
steps,
|
|
194
277
|
);
|
|
195
278
|
if (refusal) throw refusal;
|
|
196
279
|
}
|
|
@@ -221,23 +304,34 @@ export class HubActionClient {
|
|
|
221
304
|
// amounts) without it, and stores the version per action for the
|
|
222
305
|
// outcomes it pushes.
|
|
223
306
|
const operation = parseOperation(input.operation);
|
|
307
|
+
// The version the capabilities read before every prepare settled on.
|
|
224
308
|
const response = await this.send(
|
|
225
|
-
`/api/internal/actions/operations
|
|
309
|
+
`/api/internal/actions/operations?version=${this.protocolVersion}`,
|
|
226
310
|
{ ...input, operation },
|
|
227
311
|
operation.kind === 'ixo.chain.tx.v1'
|
|
228
312
|
? HUB_CHAIN_TX_PREPARE_TIMEOUT_MS
|
|
229
313
|
: HUB_TIMEOUT_MS,
|
|
230
314
|
prepareSubjectOf(operation.kind),
|
|
315
|
+
operation.kind === 'ixo.chain.tx.v1'
|
|
316
|
+
? operation.messages.map((message) => message.type)
|
|
317
|
+
: [],
|
|
231
318
|
);
|
|
232
319
|
if (!response || typeof response !== 'object' || !('reference' in response))
|
|
233
320
|
throw new Error('Hub action response has no reference');
|
|
234
321
|
return assertHubReferenceUrl(response.reference, this.origin);
|
|
235
322
|
}
|
|
236
323
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
324
|
+
/** `timeoutMs`: a turn's tool list passes a short one (a slow Hub must not hold a turn). */
|
|
325
|
+
async capabilities(
|
|
326
|
+
timeoutMs = HUB_TIMEOUT_MS,
|
|
327
|
+
): Promise<ActionCapabilitiesV1> {
|
|
328
|
+
return this.negotiated(async (version) =>
|
|
329
|
+
parseCapabilities(
|
|
330
|
+
await this.send(
|
|
331
|
+
`/api/internal/actions/operations/capabilities?version=${version}`,
|
|
332
|
+
undefined,
|
|
333
|
+
timeoutMs,
|
|
334
|
+
),
|
|
241
335
|
),
|
|
242
336
|
);
|
|
243
337
|
}
|
|
@@ -249,16 +343,18 @@ export class HubActionClient {
|
|
|
249
343
|
// Options version 3 (asked at the protocol version; version 4 answers
|
|
250
344
|
// the same): version 2 plus the owner's bank tokens
|
|
251
345
|
// (`transfer.tokens`), which any-token transfers are chosen from.
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
346
|
+
const options = await this.negotiated(async (version) => {
|
|
347
|
+
const query = new URLSearchParams({
|
|
348
|
+
bindingId: input.bindingId,
|
|
349
|
+
bindingRevision: String(input.bindingRevision),
|
|
350
|
+
version,
|
|
351
|
+
});
|
|
352
|
+
return parseActionOptions(
|
|
353
|
+
await this.send(
|
|
354
|
+
`/api/internal/actions/operations/options?${query.toString()}`,
|
|
355
|
+
),
|
|
356
|
+
);
|
|
256
357
|
});
|
|
257
|
-
const options = parseActionOptions(
|
|
258
|
-
await this.send(
|
|
259
|
-
`/api/internal/actions/operations/options?${query.toString()}`,
|
|
260
|
-
),
|
|
261
|
-
);
|
|
262
358
|
if (options.version !== 'ixo.auth-options/3')
|
|
263
359
|
throw new Error('Hub action options require version 3');
|
|
264
360
|
return options;
|
|
@@ -295,10 +391,12 @@ export class HubActionClient {
|
|
|
295
391
|
): Promise<HubActionStatus> {
|
|
296
392
|
if (!/^[A-Za-z0-9_-]{32}$/.test(id))
|
|
297
393
|
throw new Error('Invalid Hub action id');
|
|
298
|
-
const raw = await this.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
394
|
+
const raw = await this.negotiated((version) =>
|
|
395
|
+
this.send(
|
|
396
|
+
`/api/internal/actions/operations/${id}?version=${version}`,
|
|
397
|
+
undefined,
|
|
398
|
+
timeoutMs,
|
|
399
|
+
),
|
|
302
400
|
);
|
|
303
401
|
if (!isRecord(raw)) throw new Error('Invalid Hub action status');
|
|
304
402
|
const value = raw;
|
|
@@ -7,32 +7,28 @@ import {
|
|
|
7
7
|
type ChainTxMessage,
|
|
8
8
|
type ChainTxOperation,
|
|
9
9
|
} from '@ixo/action-protocol';
|
|
10
|
+
import type { ChainTxOwnerField, DistributiveOmit } from '../plugin-api/types';
|
|
10
11
|
import type { HubActionClient } from './client';
|
|
11
12
|
import { HubActionRefused, type HubActionOrigin } from './store';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* A chain-transaction message as the owner object completes it: bank
|
|
15
|
-
* amounts already in base units (`
|
|
16
|
-
* identity
|
|
15
|
+
* A chain-transaction message as the owner object completes it: bank and
|
|
16
|
+
* stake amounts already in base units (`chainTxAmountsInBaseUnits`), the
|
|
17
|
+
* owner's identity (`ChainTxOwnerField`) still to add (`chainTxOperation`).
|
|
17
18
|
*/
|
|
18
|
-
export type ChainTxHostMessage =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
>
|
|
23
|
-
| Omit<Extract<ChainTxMessage, { type: 'ixo.claims.submit' }>, 'agentDid'>
|
|
24
|
-
| Omit<
|
|
25
|
-
Extract<ChainTxMessage, { type: 'ixo.claims.evaluate' }>,
|
|
26
|
-
'agentDid' | 'agentAddress'
|
|
27
|
-
>;
|
|
19
|
+
export type ChainTxHostMessage = DistributiveOmit<
|
|
20
|
+
ChainTxMessage,
|
|
21
|
+
ChainTxOwnerField
|
|
22
|
+
>;
|
|
28
23
|
|
|
29
24
|
/**
|
|
30
25
|
* The chain transaction (`ixo.chain.tx.v1`) of the model's messages: the
|
|
31
26
|
* signer is the owner's account (their DID is `did:ixo:<address>`), and
|
|
32
|
-
* the owner's identity in
|
|
33
|
-
* `did:ixo:<signer>` (the
|
|
34
|
-
*
|
|
35
|
-
*
|
|
27
|
+
* the owner's identity in the messages is the signer's: `agentDid`,
|
|
28
|
+
* `creatorDid`, `controllerDid` and `ownerDid` are `did:ixo:<signer>` (the
|
|
29
|
+
* DID the runtime authenticates to the Hub as) and an evaluation's
|
|
30
|
+
* `agentAddress` is the signer (the chain records the evaluator, and pays
|
|
31
|
+
* it, by that address; Auth Hub requires each to be the owner's). The
|
|
36
32
|
* protocol parses the result; a field the runtime sets (signer, network,
|
|
37
33
|
* chain id) is refused in fixed words, a field from the model's messages
|
|
38
34
|
* in the protocol's (`Invalid messages[0].tokens[0].id`).
|
|
@@ -49,15 +45,26 @@ export function chainTxOperation(input: {
|
|
|
49
45
|
const signer = input.ownerDid.startsWith('did:ixo:')
|
|
50
46
|
? input.ownerDid.slice('did:ixo:'.length)
|
|
51
47
|
: '';
|
|
52
|
-
const
|
|
48
|
+
const ownDid = `did:ixo:${signer}`;
|
|
53
49
|
// Identity last: a stray identity field cannot survive.
|
|
54
|
-
const messages
|
|
55
|
-
message.type
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
const messages = input.messages.map((message): ChainTxMessage => {
|
|
51
|
+
switch (message.type) {
|
|
52
|
+
case 'ixo.claims.submit':
|
|
53
|
+
case 'ixo.claims.claim-intent':
|
|
54
|
+
case 'ixo.claims.dispute':
|
|
55
|
+
return { ...message, agentDid: ownDid };
|
|
56
|
+
case 'ixo.claims.evaluate':
|
|
57
|
+
return { ...message, agentDid: ownDid, agentAddress: signer };
|
|
58
|
+
case 'ixo.claims.create-claim-authorization':
|
|
59
|
+
return { ...message, creatorDid: ownDid };
|
|
60
|
+
case 'ixo.entity.update':
|
|
61
|
+
return { ...message, controllerDid: ownDid };
|
|
62
|
+
case 'ixo.entity.create':
|
|
63
|
+
return { ...message, ownerDid: ownDid };
|
|
64
|
+
default:
|
|
65
|
+
return message;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
61
68
|
try {
|
|
62
69
|
return parseOperation({
|
|
63
70
|
kind: 'ixo.chain.tx.v1',
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
CHAIN_TX_GROUP_MESSAGE_TYPES,
|
|
3
|
+
parseChainTxRefusal,
|
|
4
|
+
type ActionOperationV1,
|
|
5
|
+
type ActionReceiptV1,
|
|
6
|
+
type ChainTxGroupAction,
|
|
7
|
+
type ChainTxMessageType,
|
|
8
|
+
type OptionsToken,
|
|
5
9
|
} from '@ixo/action-protocol';
|
|
6
|
-
import
|
|
10
|
+
import { chainTxRefusalWords, wordableRefusal } from './chain-tx-refusals';
|
|
11
|
+
import type {
|
|
12
|
+
ChainTxGroupActionInput,
|
|
13
|
+
ChainTxMessageInput,
|
|
14
|
+
ChainTxStakeInput,
|
|
15
|
+
} from '../plugin-api/types';
|
|
7
16
|
import { quoteHubText } from './hub-text';
|
|
8
17
|
import type { ChainTxHostMessage } from './request';
|
|
9
18
|
import { HubActionRefused } from './store';
|
|
@@ -90,25 +99,147 @@ const IXO_TOKEN: TransferToken = {
|
|
|
90
99
|
verified: true,
|
|
91
100
|
};
|
|
92
101
|
|
|
102
|
+
/** `toBaseUnits`, its refusal naming the message's amount (`messages[0].amount`). */
|
|
103
|
+
function converted(at: string, amount: string, decimals: number): bigint {
|
|
104
|
+
try {
|
|
105
|
+
return toBaseUnits(amount, decimals);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof HubActionRefused)
|
|
108
|
+
throw new HubActionRefused('refused', `${at}.amount: ${error.message}`);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isStakeInput = (
|
|
114
|
+
message: ChainTxMessageInput | ChainTxGroupActionInput,
|
|
115
|
+
): message is ChainTxStakeInput =>
|
|
116
|
+
message.type === 'cosmos.staking.delegate' ||
|
|
117
|
+
message.type === 'cosmos.staking.undelegate' ||
|
|
118
|
+
message.type === 'cosmos.staking.redelegate';
|
|
119
|
+
|
|
93
120
|
/**
|
|
94
|
-
*
|
|
121
|
+
* Whether converting `messages` needs the owner's token list: a bank or
|
|
122
|
+
* stake step (its amount and the owner's balance), or a new group
|
|
123
|
+
* proposal's coin send of another token than IXO (its decimals).
|
|
124
|
+
*/
|
|
125
|
+
export function chainTxNeedsTokenList(
|
|
126
|
+
messages: readonly ChainTxMessageInput[],
|
|
127
|
+
): boolean {
|
|
128
|
+
return messages.some(
|
|
129
|
+
(message) =>
|
|
130
|
+
message.type === 'cosmos.bank.send' ||
|
|
131
|
+
message.type === 'cosmos.staking.delegate' ||
|
|
132
|
+
(message.type === 'daodao.proposal.create' &&
|
|
133
|
+
message.actions.some(
|
|
134
|
+
(action) =>
|
|
135
|
+
action.type === 'cosmos.bank.send' &&
|
|
136
|
+
(action.denom ?? IXO_TOKEN.denom) !== IXO_TOKEN.denom,
|
|
137
|
+
)),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* One action of a new group proposal with its amount in base units: a
|
|
143
|
+
* stake's IXO with 6 decimals, a coin send's with the decimals of its token
|
|
144
|
+
* (IXO without a list; another token from the owner's list, which is the
|
|
145
|
+
* only place the runtime learns a token's decimals). No balance is
|
|
146
|
+
* checked: the group's own account pays when it carries the proposal out,
|
|
147
|
+
* and the network checks it then.
|
|
148
|
+
*/
|
|
149
|
+
function groupActionInBaseUnits(
|
|
150
|
+
action: ChainTxGroupActionInput,
|
|
151
|
+
at: string,
|
|
152
|
+
tokens: readonly OptionsToken[] | null,
|
|
153
|
+
): ChainTxGroupAction {
|
|
154
|
+
if (isStakeInput(action)) {
|
|
155
|
+
const { amount, ...rest } = action;
|
|
156
|
+
return {
|
|
157
|
+
...rest,
|
|
158
|
+
amountBaseUnits: converted(at, amount, IXO_TOKEN.decimals).toString(),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (action.type !== 'cosmos.bank.send') return action;
|
|
162
|
+
const denom = action.denom ?? IXO_TOKEN.denom;
|
|
163
|
+
const token =
|
|
164
|
+
denom === IXO_TOKEN.denom
|
|
165
|
+
? IXO_TOKEN
|
|
166
|
+
: tokens?.find((item) => item.denom === denom);
|
|
167
|
+
if (!token)
|
|
168
|
+
throw new HubActionRefused(
|
|
169
|
+
'refused',
|
|
170
|
+
`${at}.denom: ${denom} is not in the owner's token list, so its decimals are unknown here; a group proposal can send IXO, or a token exactly as hub_action_options lists it.`,
|
|
171
|
+
);
|
|
172
|
+
return {
|
|
173
|
+
type: 'cosmos.bank.send',
|
|
174
|
+
toAddress: action.toAddress,
|
|
175
|
+
amountBaseUnits: converted(at, action.amount, token.decimals).toString(),
|
|
176
|
+
denom: token.denom,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A chain transaction's amounts in base units; messages without one pass
|
|
95
182
|
* unchanged. Each bank step names a display `amount` of a token from the
|
|
96
183
|
* owner's list (`denom`, absent = uixo): the token must be listed, the
|
|
97
184
|
* amount converts with its listed decimals (`toBaseUnits`: a plain decimal,
|
|
98
185
|
* no more digits after the point than the decimals, above zero; an
|
|
99
186
|
* unverified token lists 0: base units), and the bank steps of one token
|
|
100
|
-
* together stay within its listed balance.
|
|
101
|
-
*
|
|
102
|
-
*
|
|
187
|
+
* together stay within its listed balance. Each stake step (stake, unstake,
|
|
188
|
+
* move stake) names a display `amount` of IXO, converted with IXO's 6
|
|
189
|
+
* decimals; what a stake takes from the wallet counts against the listed
|
|
190
|
+
* IXO balance together with the IXO bank steps, when the list has IXO
|
|
191
|
+
* (Auth Hub checks every balance again before the card is made). A new
|
|
192
|
+
* group proposal's bank and stake actions convert the same way
|
|
193
|
+
* (`groupActionInBaseUnits`) and are never checked against the owner's
|
|
194
|
+
* balance: the group's own account pays them when the group carries the
|
|
195
|
+
* proposal out. `tokens` is the owner's list (hub_action_options
|
|
196
|
+
* `transfer.tokens`), null when the Hub listed none; the caller reads it
|
|
197
|
+
* only when `chainTxNeedsTokenList`.
|
|
103
198
|
*/
|
|
104
|
-
export function
|
|
199
|
+
export function chainTxAmountsInBaseUnits(
|
|
105
200
|
messages: readonly ChainTxMessageInput[],
|
|
106
201
|
tokens: readonly OptionsToken[] | null,
|
|
107
202
|
): ChainTxHostMessage[] {
|
|
108
203
|
const spent = new Map<string, bigint>();
|
|
109
|
-
|
|
110
|
-
|
|
204
|
+
/** The bank and stake steps of one listed token together, within its listed balance. */
|
|
205
|
+
const spend = (
|
|
206
|
+
at: string,
|
|
207
|
+
token: OptionsToken,
|
|
208
|
+
amount: bigint,
|
|
209
|
+
what: string,
|
|
210
|
+
) => {
|
|
211
|
+
const total = (spent.get(token.denom) ?? 0n) + amount;
|
|
212
|
+
spent.set(token.denom, total);
|
|
213
|
+
if (total > BigInt(token.balanceBaseUnits))
|
|
214
|
+
throw new HubActionRefused(
|
|
215
|
+
'refused',
|
|
216
|
+
`${at}.amount: the owner holds ${describeAmount(token.balanceBaseUnits, token)}, less than ${total === amount ? what : 'what the steps of this transaction take of it together'}.`,
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
return messages.map((message, index): ChainTxHostMessage => {
|
|
111
220
|
const at = `messages[${index}]`;
|
|
221
|
+
if (message.type === 'daodao.proposal.create')
|
|
222
|
+
return {
|
|
223
|
+
...message,
|
|
224
|
+
actions: message.actions.map((action, j) =>
|
|
225
|
+
groupActionInBaseUnits(action, `${at}.actions[${j}]`, tokens),
|
|
226
|
+
),
|
|
227
|
+
};
|
|
228
|
+
if (
|
|
229
|
+
message.type === 'cosmos.staking.delegate' ||
|
|
230
|
+
message.type === 'cosmos.staking.undelegate' ||
|
|
231
|
+
message.type === 'cosmos.staking.redelegate'
|
|
232
|
+
) {
|
|
233
|
+
const { amount: display, ...rest } = message;
|
|
234
|
+
const amount = converted(at, display, IXO_TOKEN.decimals);
|
|
235
|
+
// Only a stake takes IXO from the wallet: unstaking and moving stake
|
|
236
|
+
// work on what is already staked.
|
|
237
|
+
const ixo = tokens?.find((item) => item.denom === IXO_TOKEN.denom);
|
|
238
|
+
if (message.type === 'cosmos.staking.delegate' && ixo)
|
|
239
|
+
spend(at, ixo, amount, 'this stake');
|
|
240
|
+
return { ...rest, amountBaseUnits: amount.toString() };
|
|
241
|
+
}
|
|
242
|
+
if (message.type !== 'cosmos.bank.send') return message;
|
|
112
243
|
if (!tokens?.length)
|
|
113
244
|
throw new HubActionRefused(
|
|
114
245
|
'refused',
|
|
@@ -124,21 +255,8 @@ export function bankStepsInBaseUnits(
|
|
|
124
255
|
.slice(0, 10)
|
|
125
256
|
.join(', ')}).`,
|
|
126
257
|
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
amount = toBaseUnits(message.amount, token.decimals);
|
|
130
|
-
} catch (error) {
|
|
131
|
-
if (error instanceof HubActionRefused)
|
|
132
|
-
throw new HubActionRefused('refused', `${at}.amount: ${error.message}`);
|
|
133
|
-
throw error;
|
|
134
|
-
}
|
|
135
|
-
const total = (spent.get(token.denom) ?? 0n) + amount;
|
|
136
|
-
spent.set(token.denom, total);
|
|
137
|
-
if (total > BigInt(token.balanceBaseUnits))
|
|
138
|
-
throw new HubActionRefused(
|
|
139
|
-
'refused',
|
|
140
|
-
`${at}.amount: the owner holds ${describeAmount(token.balanceBaseUnits, token)}, less than ${total === amount ? 'this send' : 'the bank steps of this token in this transaction'}.`,
|
|
141
|
-
);
|
|
258
|
+
const amount = converted(at, message.amount, token.decimals);
|
|
259
|
+
spend(at, token, amount, 'this send');
|
|
142
260
|
return {
|
|
143
261
|
type: 'cosmos.bank.send',
|
|
144
262
|
toAddress: message.toAddress,
|
|
@@ -199,10 +317,26 @@ export function prepareSubjectOf(
|
|
|
199
317
|
: 'operation';
|
|
200
318
|
}
|
|
201
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Why the chain's authorization check refused a chain transaction, by its
|
|
322
|
+
* steps: a group step (the group may let only members carry proposals out
|
|
323
|
+
* or propose), a claim step (the collection admin's authorization), or
|
|
324
|
+
* neither.
|
|
325
|
+
*/
|
|
326
|
+
function notAuthorizedWhy(steps: readonly ChainTxMessageType[]): string {
|
|
327
|
+
if (steps.some((type) => GROUP_STEP_TYPES.includes(type)))
|
|
328
|
+
return ' (for a group step, the group may let only its members do this)';
|
|
329
|
+
if (steps.some((type) => type.startsWith('ixo.claims.')))
|
|
330
|
+
return ' (for claims, the collection admin has not granted the authorization)';
|
|
331
|
+
return '';
|
|
332
|
+
}
|
|
333
|
+
const GROUP_STEP_TYPES: readonly ChainTxMessageType[] =
|
|
334
|
+
CHAIN_TX_GROUP_MESSAGE_TYPES;
|
|
335
|
+
|
|
202
336
|
/** What the model is told when Auth Hub refuses a prepare (400). */
|
|
203
337
|
const HUB_PREPARE_REFUSALS: Record<
|
|
204
338
|
string,
|
|
205
|
-
(subject: HubPrepareSubject) => string
|
|
339
|
+
(subject: HubPrepareSubject, steps: readonly ChainTxMessageType[]) => string
|
|
206
340
|
> = {
|
|
207
341
|
simulation_failed: (subject) =>
|
|
208
342
|
`Auth Hub simulated this ${subject} and it would fail on chain`,
|
|
@@ -215,10 +349,11 @@ const HUB_PREPARE_REFUSALS: Record<
|
|
|
215
349
|
// it or tell the owner what was wrong.
|
|
216
350
|
operation_not_registered: (subject) =>
|
|
217
351
|
`Auth Hub did not accept this ${subject} as written`,
|
|
218
|
-
// The chain's authorization
|
|
219
|
-
// the collection admin has not authorized this account
|
|
220
|
-
|
|
221
|
-
|
|
352
|
+
// The chain's authorization check refused the simulation: for claims,
|
|
353
|
+
// the collection admin has not authorized this account; for a group step,
|
|
354
|
+
// the group lets only its members do it.
|
|
355
|
+
not_authorized: (subject, steps) =>
|
|
356
|
+
`The account is not authorized on chain for this ${subject}${notAuthorizedWhy(steps)}`,
|
|
222
357
|
};
|
|
223
358
|
/** Appended to every refusal no retry or change of arguments can lift. */
|
|
224
359
|
const NOT_FIXABLE = 'retrying or changing the fields will not help';
|
|
@@ -310,12 +445,31 @@ export function hubPrepareRefusal(
|
|
|
310
445
|
status: number,
|
|
311
446
|
body: unknown,
|
|
312
447
|
subject: HubPrepareSubject = 'transfer',
|
|
448
|
+
steps: readonly ChainTxMessageType[] = [],
|
|
313
449
|
): HubActionRefused | null {
|
|
314
450
|
const parsed = hubErrorBody(body);
|
|
315
451
|
const unfixable = unfixableRefusal(status, parsed, subject);
|
|
316
452
|
if (unfixable)
|
|
317
453
|
return new HubActionRefused('refused', unfixable, undefined, status);
|
|
318
454
|
if (!parsed) return null;
|
|
455
|
+
// A chain transaction Auth Hub would not make from what it read on chain,
|
|
456
|
+
// or whose simulation named one message: a fixed code, in the runtime's
|
|
457
|
+
// own words (the reason text is not passed on). Without a code, the
|
|
458
|
+
// reason is passed on quoted, as below.
|
|
459
|
+
const coded =
|
|
460
|
+
status === 400 &&
|
|
461
|
+
subject === 'transaction' &&
|
|
462
|
+
(parsed.error === 'operation_not_registered' ||
|
|
463
|
+
parsed.error === 'simulation_failed')
|
|
464
|
+
? wordableRefusal(parseChainTxRefusal(parsed.reason))
|
|
465
|
+
: null;
|
|
466
|
+
if (coded)
|
|
467
|
+
return new HubActionRefused(
|
|
468
|
+
'refused',
|
|
469
|
+
chainTxRefusalWords(coded),
|
|
470
|
+
undefined,
|
|
471
|
+
status,
|
|
472
|
+
);
|
|
319
473
|
const reason = parsed.reason
|
|
320
474
|
? quoteHubText('reason', parsed.reason, HUB_REASON_MAX)
|
|
321
475
|
: '';
|
|
@@ -326,7 +480,7 @@ export function hubPrepareRefusal(
|
|
|
326
480
|
if (words)
|
|
327
481
|
return new HubActionRefused(
|
|
328
482
|
'refused',
|
|
329
|
-
`${words(subject)}.${reason ? ` ${reason}.` : ''}`,
|
|
483
|
+
`${words(subject, steps)}.${reason ? ` ${reason}.` : ''}`,
|
|
330
484
|
undefined,
|
|
331
485
|
status,
|
|
332
486
|
);
|