@ixo/oracle-runtime-workers 0.28.2 → 0.29.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 +4 -4
- package/src/delivery/plan.ts +1 -1
- package/src/do/user-oracle-do.ts +42 -71
- package/src/hub-actions/client.ts +52 -21
- package/src/hub-actions/request.ts +102 -0
- package/src/hub-actions/transfer.ts +167 -18
- package/src/plugin-api/types.ts +35 -7
- package/src/plugins/hub-actions/hub-actions.plugin.ts +518 -50
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
151
|
-
* are not one of those it maps:
|
|
152
|
-
* -
|
|
153
|
-
*
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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',
|
package/src/plugin-api/types.ts
CHANGED
|
@@ -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
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
781
|
-
denom: string;
|
|
782
|
-
amount: string;
|
|
810
|
+
messages: ChainTxMessageInput[];
|
|
783
811
|
memo: string;
|
|
784
812
|
}): Promise<ActionReferenceV1>;
|
|
785
813
|
request(input: {
|