@ixo/oracle-runtime-workers 0.28.2 → 0.29.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.
@@ -1,4 +1,10 @@
1
- import type { ActionReceiptV1, OptionsToken } from '@ixo/action-protocol';
1
+ import type {
2
+ ActionOperationV1,
3
+ ActionReceiptV1,
4
+ OptionsToken,
5
+ } from '@ixo/action-protocol';
6
+ import type { ChainTxMessageInput } from '../plugin-api/types';
7
+ import type { ChainTxHostMessage } from './request';
2
8
  import { HubActionRefused } from './store';
3
9
 
4
10
  /**
@@ -83,6 +89,64 @@ const IXO_TOKEN: TransferToken = {
83
89
  verified: true,
84
90
  };
85
91
 
92
+ /**
93
+ * A chain transaction's bank steps in base units; the other messages pass
94
+ * unchanged. Each bank step names a display `amount` of a token from the
95
+ * owner's list (`denom`, absent = uixo): the token must be listed, the
96
+ * amount converts with its listed decimals (`toBaseUnits`: a plain decimal,
97
+ * no more digits after the point than the decimals, above zero; an
98
+ * unverified token lists 0: base units), and the bank steps of one token
99
+ * together stay within its listed balance. `tokens` is the owner's list
100
+ * (hub_action_options `transfer.tokens`), null when the Hub listed none;
101
+ * the caller reads it only when there is a bank step.
102
+ */
103
+ export function bankStepsInBaseUnits(
104
+ messages: readonly ChainTxMessageInput[],
105
+ tokens: readonly OptionsToken[] | null,
106
+ ): ChainTxHostMessage[] {
107
+ const spent = new Map<string, bigint>();
108
+ return messages.map((message, index) => {
109
+ if (message.type !== 'cosmos.bank.send') return message;
110
+ const at = `messages[${index}]`;
111
+ if (!tokens?.length)
112
+ throw new HubActionRefused(
113
+ 'refused',
114
+ 'Coin sends are not available for this owner right now (no token list from Auth Hub).',
115
+ );
116
+ const denom = message.denom ?? IXO_TOKEN.denom;
117
+ const token = tokens.find((item) => item.denom === denom);
118
+ if (!token)
119
+ throw new HubActionRefused(
120
+ 'refused',
121
+ `${at}.denom: ${denom} is not in the owner's token list; use a denom exactly as hub_action_options lists it (${tokens
122
+ .map((item) => item.denom)
123
+ .slice(0, 10)
124
+ .join(', ')}).`,
125
+ );
126
+ let amount: bigint;
127
+ try {
128
+ amount = toBaseUnits(message.amount, token.decimals);
129
+ } catch (error) {
130
+ if (error instanceof HubActionRefused)
131
+ throw new HubActionRefused('refused', `${at}.amount: ${error.message}`);
132
+ throw error;
133
+ }
134
+ const total = (spent.get(token.denom) ?? 0n) + amount;
135
+ spent.set(token.denom, total);
136
+ if (total > BigInt(token.balanceBaseUnits))
137
+ throw new HubActionRefused(
138
+ 'refused',
139
+ `${at}.amount: the owner holds ${describeAmount(token.balanceBaseUnits, token)}, less than ${total === amount ? 'this send' : 'the bank steps of this token in this transaction'}.`,
140
+ );
141
+ return {
142
+ type: 'cosmos.bank.send',
143
+ toAddress: message.toAddress,
144
+ amountBaseUnits: amount.toString(),
145
+ denom: token.denom,
146
+ };
147
+ });
148
+ }
149
+
86
150
  /**
87
151
  * The owner-facing line for a bank receipt (v2 receipts carry `denom` and
88
152
  * `amountBaseUnits`), or null for any other receipt. Without a stored token
@@ -116,15 +180,90 @@ export function bankReceiptSummary(
116
180
  return `${what} ${receipt.code === 0 ? 'confirmed' : 'failed'} on chain. Transaction ${receipt.txHash}, block ${receipt.height}.`;
117
181
  }
118
182
 
119
- /** What the model is told when Auth Hub refuses a transfer at preparation (400). */
120
- const HUB_TRANSFER_REFUSALS: Record<string, string> = {
121
- simulation_failed:
122
- 'Auth Hub simulated this transfer and it would fail on chain',
123
- fee_above_ceiling:
124
- "The network fee for this transfer is above this oracle's fee limit",
125
- insufficient_funds:
126
- 'The account does not hold enough to cover this transfer and its network fee',
183
+ /**
184
+ * What a refused prepare is called in the words the model relays: a bank
185
+ * send (`ixo.bank.send.v1`) is a transfer, a chain transaction
186
+ * (`ixo.chain.tx.v1`, one or more messages) a transaction, anything else an
187
+ * operation.
188
+ */
189
+ export type HubPrepareSubject = 'transfer' | 'transaction' | 'operation';
190
+
191
+ export function prepareSubjectOf(
192
+ kind: ActionOperationV1['kind'],
193
+ ): HubPrepareSubject {
194
+ return kind === 'ixo.bank.send.v1'
195
+ ? 'transfer'
196
+ : kind === 'ixo.chain.tx.v1'
197
+ ? 'transaction'
198
+ : 'operation';
199
+ }
200
+
201
+ /** What the model is told when Auth Hub refuses a prepare (400). */
202
+ const HUB_PREPARE_REFUSALS: Record<
203
+ string,
204
+ (subject: HubPrepareSubject) => string
205
+ > = {
206
+ simulation_failed: (subject) =>
207
+ `Auth Hub simulated this ${subject} and it would fail on chain`,
208
+ fee_above_ceiling: (subject) =>
209
+ `The network fee for this ${subject} is above this oracle's fee limit`,
210
+ insufficient_funds: (subject) =>
211
+ `The account does not hold enough to cover this ${subject} and its network fee`,
212
+ // Chain transactions: the Hub names the field it refused in `reason`
213
+ // ("invalid_message: messages[1].toAddress"), so the model can correct
214
+ // it or tell the owner what was wrong.
215
+ operation_not_registered: (subject) =>
216
+ `Auth Hub did not accept this ${subject} as written`,
217
+ // The chain's authorization module refused the simulation: for claims,
218
+ // the collection admin has not authorized this account.
219
+ not_authorized: (subject) =>
220
+ `The account is not authorized on chain for this ${subject} (for claims, the collection admin has not granted the authorization)`,
127
221
  };
222
+ /** Appended to every refusal no retry or change of arguments can lift. */
223
+ const NOT_FIXABLE = 'retrying or changing the fields will not help';
224
+
225
+ /** A kind the account's operator turned off, in the subject's words. */
226
+ function kindOff(subject: HubPrepareSubject): string {
227
+ return subject === 'transfer'
228
+ ? 'Bank sends are'
229
+ : subject === 'transaction'
230
+ ? 'Chain transactions are'
231
+ : 'This kind of operation is';
232
+ }
233
+
234
+ /**
235
+ * Hub refusals the model cannot fix (fixed words, no retry): the kind
236
+ * turned off for the account, a field the runtime sets (signer, network,
237
+ * chain id), a protocol or request the Hub does not accept from this
238
+ * runtime, and no account or link for the owner (404). A 429 passes with
239
+ * time, but not within a turn.
240
+ */
241
+ function unfixableRefusal(
242
+ status: number,
243
+ parsed: { error: string; reason?: string } | null,
244
+ subject: HubPrepareSubject,
245
+ ): string | null {
246
+ if (status === 404)
247
+ return `Auth Hub has no completed account or active channel link for this owner (${parsed?.error ?? 'not_found'}); ${NOT_FIXABLE}.`;
248
+ if (status === 429)
249
+ return `Auth Hub is limiting requests from this oracle right now (${parsed?.error ?? 'rate_limited'}); try again later.`;
250
+ if (status !== 400 || !parsed) return null;
251
+ if (parsed.error === 'operation_not_registered') {
252
+ if (parsed.reason === 'kind_disabled')
253
+ return `${kindOff(subject)} turned off for this oracle account in Auth Hub (kind_disabled); ${NOT_FIXABLE}.`;
254
+ const field = /^invalid_operation: (signer|network|chainId)$/.exec(
255
+ parsed.reason ?? '',
256
+ )?.[1];
257
+ if (field)
258
+ return `Auth Hub refused this ${subject}'s ${field}, which the runtime sets from the owner's account and this oracle's configuration (${parsed.reason}); ${NOT_FIXABLE}.`;
259
+ }
260
+ if (parsed.error === 'protocol_version_required')
261
+ return `Auth Hub does not accept this ${subject} at this runtime's protocol version (protocol_version_required); ${NOT_FIXABLE}.`;
262
+ if (parsed.error === 'invalid_request')
263
+ return `Auth Hub refused the request itself, not its fields (invalid_request); ${NOT_FIXABLE}.`;
264
+ return null;
265
+ }
266
+
128
267
  /** Hub codes that pass with time (a sequence race, the transfer path down). */
129
268
  const HUB_TRANSIENT_CODES: Readonly<Record<number, readonly string[]>> = {
130
269
  409: ['sequence_mismatch'],
@@ -147,10 +286,16 @@ function hubErrorBody(
147
286
 
148
287
  /**
149
288
  * A Hub refusal of a prepare (`{error, reason?}` with status 400, 409 or
150
- * 503) as a refusal the model can relay, or null when the status and code
151
- * are not one of those it maps:
152
- * - 400 `simulation_failed`, `fee_above_ceiling`, `insufficient_funds`: the
153
- * transfer as asked cannot go ahead (with the Hub's reason);
289
+ * 503, or any 404 or 429) as a refusal the model can relay, or null when
290
+ * the status and code are not one of those it maps:
291
+ * - fixed words that no retry or change of fields helps: 400
292
+ * `operation_not_registered` with reason `kind_disabled` or
293
+ * `invalid_operation: signer|network|chainId`, 400
294
+ * `protocol_version_required` and `invalid_request`, and 404; 429 says to
295
+ * try again later;
296
+ * - 400 `simulation_failed`, `fee_above_ceiling`, `insufficient_funds`,
297
+ * `not_authorized`, `operation_not_registered`: the transfer or
298
+ * transaction as asked cannot go ahead (with the Hub's reason);
154
299
  * - 409 `sequence_mismatch`, 503 `transfer_unavailable`: temporary, try
155
300
  * again shortly;
156
301
  * - any other 409: the account may not use this operation (deterministic,
@@ -159,15 +304,19 @@ function hubErrorBody(
159
304
  export function hubPrepareRefusal(
160
305
  status: number,
161
306
  body: unknown,
307
+ subject: HubPrepareSubject = 'transfer',
162
308
  ): HubActionRefused | null {
163
309
  const parsed = hubErrorBody(body);
310
+ const unfixable = unfixableRefusal(status, parsed, subject);
311
+ if (unfixable) return new HubActionRefused('refused', unfixable);
164
312
  if (!parsed) return null;
165
313
  const detail = parsed.reason ? `: ${parsed.reason}` : '';
166
- if (status === 400 && Object.hasOwn(HUB_TRANSFER_REFUSALS, parsed.error))
167
- return new HubActionRefused(
168
- 'refused',
169
- `${HUB_TRANSFER_REFUSALS[parsed.error]}${detail}.`,
170
- );
314
+ const words =
315
+ status === 400 && Object.hasOwn(HUB_PREPARE_REFUSALS, parsed.error)
316
+ ? HUB_PREPARE_REFUSALS[parsed.error]
317
+ : undefined;
318
+ if (words)
319
+ return new HubActionRefused('refused', `${words(subject)}${detail}.`);
171
320
  if (HUB_TRANSIENT_CODES[status]?.includes(parsed.error))
172
321
  return new HubActionRefused(
173
322
  'refused',
@@ -24,8 +24,32 @@ import type {
24
24
  ActionReferenceV1,
25
25
  ActionOutcomeV1,
26
26
  ActionCapabilitiesV1,
27
+ ChainTxMessage,
27
28
  } from '@ixo/action-protocol';
28
29
 
30
+ /**
31
+ * A chain-transaction message as the model writes it: the protocol's
32
+ * message without the owner's identity, which the host fills (a claim's
33
+ * `agentDid`, an evaluation's `agentDid` and `agentAddress`: the signer's).
34
+ * A bank step names a display `amount` of a token from the owner's list
35
+ * (`denom`, absent = uixo); the host converts it to the protocol's
36
+ * `amountBaseUnits` from the listed decimals.
37
+ */
38
+ export type ChainTxMessageInput =
39
+ | {
40
+ type: 'cosmos.bank.send';
41
+ toAddress: string;
42
+ /** A plain decimal in the token's display units ("1.5"). */
43
+ amount: string;
44
+ denom?: string;
45
+ }
46
+ | Extract<ChainTxMessage, { type: 'ixo.token.transfer' }>
47
+ | Omit<Extract<ChainTxMessage, { type: 'ixo.claims.submit' }>, 'agentDid'>
48
+ | Omit<
49
+ Extract<ChainTxMessage, { type: 'ixo.claims.evaluate' }>,
50
+ 'agentDid' | 'agentAddress'
51
+ >;
52
+
29
53
  /**
30
54
  * Route shape returned by `OraclePlugin.getAuthExcludedRoutes()`. Paths are
31
55
  * matched against the Hono router's request path (leading slash optional).
@@ -764,11 +788,17 @@ export interface RuntimeContext<TConfig = MergedConfig> {
764
788
  bindingRevision: number;
765
789
  }): Promise<import('@ixo/action-protocol').HubActionOptionsV3>;
766
790
  /**
767
- * A bank send of a token from the owner's options list: `amount` is a
768
- * decimal in the token's display units; the host converts it to base
769
- * units from the listed decimals and builds the operation (no fee).
791
+ * One chain transaction (`ixo.chain.tx.v1`) of 1–8 messages: the host
792
+ * adds the chain id and network (the account's), the signer (the
793
+ * owner's address) and the owner's identity in claims messages
794
+ * (`agentDid` = `did:ixo:<signer>`, an evaluation's `agentAddress` = the
795
+ * signer) and prepares it like any operation (no fee: the Hub simulates
796
+ * gas and sets it). A bank step's display `amount` is converted to base
797
+ * units from the owner's token list (one options read per call, only
798
+ * when there is a bank step), refused for a token not listed, more
799
+ * digits than its decimals, zero or more than the listed balance.
770
800
  */
771
- transfer(input: {
801
+ chainTransaction(input: {
772
802
  ownerDid: string;
773
803
  sessionId: string;
774
804
  runId: string;
@@ -777,9 +807,7 @@ export interface RuntimeContext<TConfig = MergedConfig> {
777
807
  bindingId: string;
778
808
  bindingRevision: number;
779
809
  requestKey: string;
780
- toAddress: string;
781
- denom: string;
782
- amount: string;
810
+ messages: ChainTxMessageInput[];
783
811
  memo: string;
784
812
  }): Promise<ActionReferenceV1>;
785
813
  request(input: {