@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixo/oracle-runtime-workers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.1",
|
|
4
4
|
"description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"!src/plugins/pod-creator/test-fixtures.ts"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@ixo/action-protocol": "0.
|
|
37
|
+
"@ixo/action-protocol": "0.9.1",
|
|
38
38
|
"@blocknote/server-util": "0.29.1",
|
|
39
39
|
"@composio/core": "0.10.0",
|
|
40
40
|
"@composio/langchain": "0.9.2",
|
package/src/do/user-oracle-do.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { chainTxOperation, prepareHubAction } from '../hub-actions/request';
|
|
|
55
55
|
import {
|
|
56
56
|
bankReceiptSummary,
|
|
57
57
|
chainTxAmountsInBaseUnits,
|
|
58
|
+
chainTxNeedsTokenList,
|
|
58
59
|
} from '../hub-actions/transfer';
|
|
59
60
|
import {
|
|
60
61
|
HUB_CHECK_AFTER_EXPIRY_MS,
|
|
@@ -3652,8 +3653,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3652
3653
|
* steps an amount of IXO: with at least one of either, the owner's
|
|
3653
3654
|
* options are read once, fresh (the balances must be current), and each
|
|
3654
3655
|
* amount is converted to base units and checked against the listed
|
|
3655
|
-
* balance (`chainTxAmountsInBaseUnits`).
|
|
3656
|
-
*
|
|
3656
|
+
* balance (`chainTxAmountsInBaseUnits`). A new group proposal's coin
|
|
3657
|
+
* send of another token than IXO reads them too, for the token's
|
|
3658
|
+
* decimals only. No fee is sent: the Hub simulates gas and sets it.
|
|
3657
3659
|
*/
|
|
3658
3660
|
private async chainTransactionHubAction(input: {
|
|
3659
3661
|
ownerDid: string;
|
|
@@ -3684,11 +3686,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
3684
3686
|
const chainId = chainIdsFromConfig(chainIdConfigSchema.parse(this.env))[
|
|
3685
3687
|
HUB_NETWORK_CHAIN[capabilities.network]
|
|
3686
3688
|
];
|
|
3687
|
-
const tokens = input.messages
|
|
3688
|
-
(message) =>
|
|
3689
|
-
message.type === 'cosmos.bank.send' ||
|
|
3690
|
-
message.type === 'cosmos.staking.delegate',
|
|
3691
|
-
)
|
|
3689
|
+
const tokens = chainTxNeedsTokenList(input.messages)
|
|
3692
3690
|
? ((await this.hubActionOptions(input, true)).transfer?.tokens ?? null)
|
|
3693
3691
|
: null;
|
|
3694
3692
|
const operation = chainTxOperation({
|
|
@@ -41,11 +41,11 @@ export const CHAIN_TX_REFUSAL_WORDS = {
|
|
|
41
41
|
},
|
|
42
42
|
proposal_not_found: {
|
|
43
43
|
means: 'there is no proposal with that number',
|
|
44
|
-
next: 'Find the
|
|
44
|
+
next: 'Find the proposal again: chain_lookup gov_proposals for a network proposal, dao_proposals for a group proposal (copy its `id`, `proposalModule` and `dao`).',
|
|
45
45
|
},
|
|
46
46
|
proposal_not_open: {
|
|
47
47
|
means:
|
|
48
|
-
'voting on this proposal is not open (it has closed, or not started yet)',
|
|
48
|
+
'voting on this proposal is not open (it has closed, or a network proposal has not started yet)',
|
|
49
49
|
next: 'Tell them; a vote cannot be recorded now.',
|
|
50
50
|
},
|
|
51
51
|
invalid_vote_option: {
|
|
@@ -168,6 +168,90 @@ export const CHAIN_TX_REFUSAL_WORDS = {
|
|
|
168
168
|
means: 'one of the identifiers is not a valid ID on the network',
|
|
169
169
|
next: 'Copy it again exactly from a lookup.',
|
|
170
170
|
},
|
|
171
|
+
group_not_supported: {
|
|
172
|
+
means:
|
|
173
|
+
'this group runs a version these cards don’t support yet, or the proposal ends at a block number rather than a time',
|
|
174
|
+
next: 'Do not retry; tell them to use the group’s own app for this.',
|
|
175
|
+
},
|
|
176
|
+
not_in_group: {
|
|
177
|
+
means: 'that proposal list is not one of this group’s',
|
|
178
|
+
next: 'Copy `dao` and `proposalModule` again together from chain_lookup dao_proposals or daos.',
|
|
179
|
+
},
|
|
180
|
+
group_paused: {
|
|
181
|
+
means:
|
|
182
|
+
'the group is paused, so a proposal with actions can’t be carried out right now',
|
|
183
|
+
next: 'Do not retry now; tell them it can be carried out once the group is no longer paused.',
|
|
184
|
+
},
|
|
185
|
+
no_voting_power: {
|
|
186
|
+
means: 'they had no vote in this group when the proposal started',
|
|
187
|
+
next: 'Do not retry; tell them only members with a vote when it started can vote on it.',
|
|
188
|
+
},
|
|
189
|
+
already_voted: {
|
|
190
|
+
means:
|
|
191
|
+
'they have already voted on this proposal, and this group doesn’t allow changing a vote',
|
|
192
|
+
next: 'Do not retry; tell them their earlier vote stands.',
|
|
193
|
+
},
|
|
194
|
+
same_vote: {
|
|
195
|
+
means: 'they have already voted this way on this proposal',
|
|
196
|
+
next: 'Nothing needs to change; tell them.',
|
|
197
|
+
},
|
|
198
|
+
not_a_member: {
|
|
199
|
+
means: 'this group lets only its members do this',
|
|
200
|
+
next: 'Do not retry; tell them a member of the group has to do it.',
|
|
201
|
+
},
|
|
202
|
+
proposal_not_passed: {
|
|
203
|
+
means:
|
|
204
|
+
'this proposal can’t be carried out: it hasn’t passed (yet, or at all), or it was already carried out',
|
|
205
|
+
next: 'Check its status with chain_lookup dao_proposals and tell them.',
|
|
206
|
+
},
|
|
207
|
+
proposal_not_rejected: {
|
|
208
|
+
means: 'only a proposal that did not pass can be closed',
|
|
209
|
+
next: 'Check its status with chain_lookup dao_proposals and tell them.',
|
|
210
|
+
},
|
|
211
|
+
group_actions_would_fail: {
|
|
212
|
+
means:
|
|
213
|
+
'if carried out now, what the proposal makes the group do would fail (in most groups that would mark the proposal failed for good)',
|
|
214
|
+
next: 'Do not retry now; tell them it can be carried out once that is fixed (for example once the group holds enough).',
|
|
215
|
+
},
|
|
216
|
+
deposit_not_supported: {
|
|
217
|
+
means:
|
|
218
|
+
'this group takes its proposal deposit in a token these cards can’t pay',
|
|
219
|
+
next: 'Do not retry; tell them to make the proposal in the group’s own app.',
|
|
220
|
+
},
|
|
221
|
+
proposal_too_large: {
|
|
222
|
+
means: 'the proposal is larger than this group allows',
|
|
223
|
+
next: 'Ask for a shorter description or fewer actions.',
|
|
224
|
+
},
|
|
225
|
+
not_for_groups: {
|
|
226
|
+
means: 'a group can’t do this kind of action through a card',
|
|
227
|
+
next: 'Leave that action out of the proposal and tell them.',
|
|
228
|
+
},
|
|
229
|
+
not_group_owned: {
|
|
230
|
+
means: 'the group does not own that entity',
|
|
231
|
+
next: 'Check the entity’s owner with chain_lookup entity; only an entity the group owns can be used.',
|
|
232
|
+
},
|
|
233
|
+
group_not_found: {
|
|
234
|
+
means:
|
|
235
|
+
'that address is not the group or its proposal list (the two may be swapped, or it is not a group contract at all)',
|
|
236
|
+
next: 'Re-check `dao` (the group) and `proposalModule` (its proposal list) with chain_lookup dao_proposals, copy them again and try again.',
|
|
237
|
+
},
|
|
238
|
+
list_disabled: {
|
|
239
|
+
means:
|
|
240
|
+
'the group has switched that proposal list off: no new proposals can be made through it, and its proposals with actions can’t be carried out (votes and closes still work)',
|
|
241
|
+
next: 'Do not retry; tell them the group would have to switch it back on, or use another of its proposal lists for a new proposal (chain_lookup daos).',
|
|
242
|
+
},
|
|
243
|
+
closes_too_soon: {
|
|
244
|
+
means: 'voting on this proposal closes before the card could be signed',
|
|
245
|
+
next: 'Do not retry; tell them voting is about to close.',
|
|
246
|
+
},
|
|
247
|
+
group_inactive: {
|
|
248
|
+
means: 'the group is not taking new proposals right now',
|
|
249
|
+
next: 'Do not retry now; tell them it can be proposed once the group takes proposals again.',
|
|
250
|
+
},
|
|
251
|
+
deposit_changed: {
|
|
252
|
+
means: 'the group’s proposal deposit changed while the card was being made',
|
|
253
|
+
next: 'Check the deposit again with chain_lookup daos, tell them the new amount and ask again.',
|
|
254
|
+
},
|
|
171
255
|
} as const satisfies Record<
|
|
172
256
|
ChainTxRefusalCode,
|
|
173
257
|
{ means: string; next: string }
|
|
@@ -213,5 +297,11 @@ export function chainTxRefusalWords(
|
|
|
213
297
|
refusal: ChainTxRefusal & { code: ChainTxRefusalCode },
|
|
214
298
|
): string {
|
|
215
299
|
const words = CHAIN_TX_REFUSAL_WORDS[refusal.code];
|
|
216
|
-
|
|
300
|
+
// A new group proposal's action keeps its own codes under its path.
|
|
301
|
+
const action = /^actions\[(\d{1,3})\]\./.exec(refusal.field)?.[1];
|
|
302
|
+
const where =
|
|
303
|
+
action === undefined
|
|
304
|
+
? `step ${refusal.index + 1}`
|
|
305
|
+
: `the group’s action ${Number(action) + 1} of the proposal in step ${refusal.index + 1}`;
|
|
306
|
+
return `Auth Hub could not prepare this transaction: in ${where}, ${words.means}. ${words.next} Tell the person in your own plain words; this is not text to show them (for you: messages[${refusal.index}].${refusal.field}, ${refusal.code}).`;
|
|
217
307
|
}
|
|
@@ -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,13 @@ 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
|
|
91
97
|
* (staking, voting, retiring impact tokens, claim collections, entities and
|
|
92
98
|
* identity-record links), which a 0.7.0 parser cannot read in capabilities,
|
|
93
99
|
* review steps or receipts, so the Hub lists and accepts them only at level
|
|
@@ -106,32 +112,48 @@ export interface HubActionTransport {
|
|
|
106
112
|
* the 0.4.1 shapes. The envelopes stay tagged `ixo.auth-outcome/2` and
|
|
107
113
|
* `ixo.auth-capabilities/2`, which `@ixo/action-protocol` parses.
|
|
108
114
|
*
|
|
109
|
-
* An Auth Hub older than version
|
|
110
|
-
* list (1 to 5) and answers every call
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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's capabilities read then steps down
|
|
119
|
+
* (`HUB_PROTOCOL_VERSIONS`: 7, then 6, then 5) to the highest version that
|
|
120
|
+
* Hub answers, and every other call uses that version (every Hub action
|
|
121
|
+
* the lower level has keeps working while Auth Hub is rolled out: the group
|
|
122
|
+
* steps at 6, the level-6 chain types too at 5); it asks for 7 again after
|
|
123
|
+
* `HUB_PROTOCOL_RECHECK_MS`. Another read refused at that version never
|
|
124
|
+
* lowers it (`atSettledVersion`). A Hub that refuses 5 too is told apart
|
|
125
|
+
* with `HUB_UPDATE_REQUIRED`.
|
|
116
126
|
*/
|
|
117
|
-
const HUB_PROTOCOL_VERSION = '
|
|
118
|
-
/** The lowest version this runtime works at:
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
const HUB_PROTOCOL_VERSION = '7';
|
|
128
|
+
/** The lowest version this runtime works at: 7 without the group steps and the level-6 chain types. */
|
|
129
|
+
const HUB_PROTOCOL_LOWEST_VERSION = '5';
|
|
130
|
+
/** Every version this runtime speaks, highest first. */
|
|
131
|
+
const HUB_PROTOCOL_VERSIONS = [
|
|
132
|
+
HUB_PROTOCOL_VERSION,
|
|
133
|
+
'6',
|
|
134
|
+
HUB_PROTOCOL_LOWEST_VERSION,
|
|
135
|
+
] as const;
|
|
136
|
+
type HubProtocolVersion = (typeof HUB_PROTOCOL_VERSIONS)[number];
|
|
137
|
+
/** How long a Hub that refused a version is spoken to at a lower one before the highest is tried again. */
|
|
124
138
|
export const HUB_PROTOCOL_RECHECK_MS = 10 * 60_000;
|
|
125
139
|
|
|
126
140
|
/**
|
|
127
141
|
* What the model is told when Auth Hub refuses a read (capabilities,
|
|
128
142
|
* options, status) with 400 `invalid_request`. A read carries nothing but
|
|
129
143
|
* the protocol version (and, for options, the binding the runtime itself
|
|
130
|
-
* checked), so that answer comes from a Hub that does not know version
|
|
131
|
-
* The owner object keeps every action it holds: polls and
|
|
132
|
-
* the outcomes up once the Hub is updated.
|
|
144
|
+
* checked), so that answer comes from a Hub that does not know the version
|
|
145
|
+
* declared. The owner object keeps every action it holds: polls and
|
|
146
|
+
* callbacks pick the outcomes up once the Hub is updated.
|
|
133
147
|
*/
|
|
134
|
-
export const HUB_UPDATE_REQUIRED = `Auth Hub must be updated before this oracle can use Hub actions: it does not accept protocol version ${
|
|
148
|
+
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.`;
|
|
149
|
+
/**
|
|
150
|
+
* A read (options, status) Auth Hub refused at the version its
|
|
151
|
+
* capabilities read accepts: a fault on that route, not an older Hub.
|
|
152
|
+
*/
|
|
153
|
+
export function hubReadRefused(version: string): string {
|
|
154
|
+
return `Auth Hub refused this read at protocol version ${version}, which it accepts for this oracle's other calls (invalid_request). Tell the person this is not available right now and try again later; changing the fields will not help.`;
|
|
155
|
+
}
|
|
156
|
+
|
|
135
157
|
/** Every Hub call but a chain-transaction prepare. */
|
|
136
158
|
const HUB_TIMEOUT_MS = 10_000;
|
|
137
159
|
/**
|
|
@@ -155,7 +177,9 @@ function isUpdateRequired(error: unknown): boolean {
|
|
|
155
177
|
|
|
156
178
|
export class HubActionClient {
|
|
157
179
|
private readonly origin: string;
|
|
158
|
-
/**
|
|
180
|
+
/** The lower version this Hub answered after refusing a higher one. */
|
|
181
|
+
private fallbackVersion: HubProtocolVersion = HUB_PROTOCOL_VERSION;
|
|
182
|
+
/** Until when this Hub is spoken to at `fallbackVersion`. */
|
|
159
183
|
private fallbackUntil = 0;
|
|
160
184
|
|
|
161
185
|
constructor(private readonly transport: HubActionTransport) {
|
|
@@ -179,37 +203,87 @@ export class HubActionClient {
|
|
|
179
203
|
/** The protocol version the next call declares. */
|
|
180
204
|
get protocolVersion(): HubProtocolVersion {
|
|
181
205
|
return Date.now() < this.fallbackUntil
|
|
182
|
-
?
|
|
206
|
+
? this.fallbackVersion
|
|
183
207
|
: HUB_PROTOCOL_VERSION;
|
|
184
208
|
}
|
|
185
209
|
|
|
186
210
|
/**
|
|
187
|
-
*
|
|
188
|
-
* unknown, the same read at version
|
|
189
|
-
* `
|
|
190
|
-
*
|
|
211
|
+
* The capabilities read at the current version; when Auth Hub refuses it
|
|
212
|
+
* as unknown, the same read at each lower version in turn
|
|
213
|
+
* (`HUB_PROTOCOL_VERSIONS`), and the version it answered is remembered for
|
|
214
|
+
* `HUB_PROTOCOL_RECHECK_MS`. Only the capabilities read decides the
|
|
215
|
+
* version: a refused read changed nothing, while a prepare's 400 can be a
|
|
216
|
+
* real refusal of its operation, and another route refusing a version
|
|
217
|
+
* (an options route bug once refused 7 and 6) must never downgrade the
|
|
218
|
+
* operations. A Hub that refuses the lowest version too answers
|
|
219
|
+
* `HUB_UPDATE_REQUIRED`.
|
|
191
220
|
*/
|
|
192
221
|
private async negotiated<T>(
|
|
193
222
|
read: (version: HubProtocolVersion) => Promise<T>,
|
|
223
|
+
): Promise<T> {
|
|
224
|
+
const current = this.protocolVersion;
|
|
225
|
+
const versions = HUB_PROTOCOL_VERSIONS.slice(
|
|
226
|
+
HUB_PROTOCOL_VERSIONS.indexOf(current),
|
|
227
|
+
);
|
|
228
|
+
for (const [step, version] of versions.entries()) {
|
|
229
|
+
try {
|
|
230
|
+
const answer = await read(version);
|
|
231
|
+
if (step > 0) {
|
|
232
|
+
this.fallbackVersion = version;
|
|
233
|
+
this.fallbackUntil = Date.now() + HUB_PROTOCOL_RECHECK_MS;
|
|
234
|
+
}
|
|
235
|
+
return answer;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (step === versions.length - 1 || !isUpdateRequired(error))
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Unreachable: the loop returns or throws at its last version.
|
|
242
|
+
throw new Error('No Hub protocol version to read at');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Another read (options, status) at the version the capabilities read
|
|
247
|
+
* settled on. When Auth Hub refuses it as unknown, the capabilities read
|
|
248
|
+
* settles the version again (a Hub rolled back since); only when that
|
|
249
|
+
* changes the version is the read tried once more, at the new version.
|
|
250
|
+
* Otherwise this read fails on its own (`hubReadRefused`) and the version
|
|
251
|
+
* the operations use stays as it is. `timeoutMs` bounds that capabilities
|
|
252
|
+
* read as it bounds this one.
|
|
253
|
+
*/
|
|
254
|
+
private async atSettledVersion<T>(
|
|
255
|
+
read: (version: HubProtocolVersion) => Promise<T>,
|
|
256
|
+
timeoutMs = HUB_TIMEOUT_MS,
|
|
194
257
|
): Promise<T> {
|
|
195
258
|
const version = this.protocolVersion;
|
|
196
259
|
try {
|
|
197
260
|
return await read(version);
|
|
198
261
|
} catch (error) {
|
|
199
|
-
if (
|
|
200
|
-
throw error;
|
|
201
|
-
const answer = await read(HUB_PROTOCOL_FALLBACK_VERSION);
|
|
202
|
-
this.fallbackUntil = Date.now() + HUB_PROTOCOL_RECHECK_MS;
|
|
203
|
-
return answer;
|
|
262
|
+
if (!isUpdateRequired(error)) throw error;
|
|
204
263
|
}
|
|
264
|
+
await this.capabilities(timeoutMs);
|
|
265
|
+
const settled = this.protocolVersion;
|
|
266
|
+
if (settled === version)
|
|
267
|
+
throw new HubActionRefused(
|
|
268
|
+
'refused',
|
|
269
|
+
hubReadRefused(version),
|
|
270
|
+
undefined,
|
|
271
|
+
400,
|
|
272
|
+
);
|
|
273
|
+
return read(settled);
|
|
205
274
|
}
|
|
206
275
|
|
|
207
|
-
/**
|
|
276
|
+
/**
|
|
277
|
+
* `subject`: what a refused prepare is called in the model's words;
|
|
278
|
+
* `steps`: a chain transaction's message types, for the words of a refusal
|
|
279
|
+
* that depends on them.
|
|
280
|
+
*/
|
|
208
281
|
private async send(
|
|
209
282
|
path: string,
|
|
210
283
|
body?: unknown,
|
|
211
284
|
timeoutMs = HUB_TIMEOUT_MS,
|
|
212
285
|
subject?: HubPrepareSubject,
|
|
286
|
+
steps?: readonly ChainTxMessageType[],
|
|
213
287
|
): Promise<unknown> {
|
|
214
288
|
const request = new Request(new URL(path, this.origin), {
|
|
215
289
|
method: body === undefined ? 'GET' : 'POST',
|
|
@@ -243,6 +317,7 @@ export class HubActionClient {
|
|
|
243
317
|
response.status,
|
|
244
318
|
await response.json().catch(() => null),
|
|
245
319
|
subject,
|
|
320
|
+
steps,
|
|
246
321
|
);
|
|
247
322
|
if (refusal) throw refusal;
|
|
248
323
|
}
|
|
@@ -281,6 +356,9 @@ export class HubActionClient {
|
|
|
281
356
|
? HUB_CHAIN_TX_PREPARE_TIMEOUT_MS
|
|
282
357
|
: HUB_TIMEOUT_MS,
|
|
283
358
|
prepareSubjectOf(operation.kind),
|
|
359
|
+
operation.kind === 'ixo.chain.tx.v1'
|
|
360
|
+
? operation.messages.map((message) => message.type)
|
|
361
|
+
: [],
|
|
284
362
|
);
|
|
285
363
|
if (!response || typeof response !== 'object' || !('reference' in response))
|
|
286
364
|
throw new Error('Hub action response has no reference');
|
|
@@ -309,7 +387,7 @@ export class HubActionClient {
|
|
|
309
387
|
// Options version 3 (asked at the protocol version; version 4 answers
|
|
310
388
|
// the same): version 2 plus the owner's bank tokens
|
|
311
389
|
// (`transfer.tokens`), which any-token transfers are chosen from.
|
|
312
|
-
const options = await this.
|
|
390
|
+
const options = await this.atSettledVersion(async (version) => {
|
|
313
391
|
const query = new URLSearchParams({
|
|
314
392
|
bindingId: input.bindingId,
|
|
315
393
|
bindingRevision: String(input.bindingRevision),
|
|
@@ -357,12 +435,14 @@ export class HubActionClient {
|
|
|
357
435
|
): Promise<HubActionStatus> {
|
|
358
436
|
if (!/^[A-Za-z0-9_-]{32}$/.test(id))
|
|
359
437
|
throw new Error('Invalid Hub action id');
|
|
360
|
-
const raw = await this.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
438
|
+
const raw = await this.atSettledVersion(
|
|
439
|
+
(version) =>
|
|
440
|
+
this.send(
|
|
441
|
+
`/api/internal/actions/operations/${id}?version=${version}`,
|
|
442
|
+
undefined,
|
|
443
|
+
timeoutMs,
|
|
444
|
+
),
|
|
445
|
+
timeoutMs,
|
|
366
446
|
);
|
|
367
447
|
if (!isRecord(raw)) throw new Error('Invalid Hub action status');
|
|
368
448
|
const value = raw;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CHAIN_TX_GROUP_MESSAGE_TYPES,
|
|
2
3
|
parseChainTxRefusal,
|
|
3
4
|
type ActionOperationV1,
|
|
4
5
|
type ActionReceiptV1,
|
|
6
|
+
type ChainTxGroupAction,
|
|
7
|
+
type ChainTxMessageType,
|
|
5
8
|
type OptionsToken,
|
|
6
9
|
} from '@ixo/action-protocol';
|
|
7
10
|
import { chainTxRefusalWords, wordableRefusal } from './chain-tx-refusals';
|
|
8
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
ChainTxGroupActionInput,
|
|
13
|
+
ChainTxMessageInput,
|
|
14
|
+
ChainTxStakeInput,
|
|
15
|
+
} from '../plugin-api/types';
|
|
9
16
|
import { quoteHubText } from './hub-text';
|
|
10
17
|
import type { ChainTxHostMessage } from './request';
|
|
11
18
|
import { HubActionRefused } from './store';
|
|
@@ -92,6 +99,84 @@ const IXO_TOKEN: TransferToken = {
|
|
|
92
99
|
verified: true,
|
|
93
100
|
};
|
|
94
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
|
+
|
|
120
|
+
/**
|
|
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
|
+
|
|
95
180
|
/**
|
|
96
181
|
* A chain transaction's amounts in base units; messages without one pass
|
|
97
182
|
* unchanged. Each bank step names a display `amount` of a token from the
|
|
@@ -103,10 +188,13 @@ const IXO_TOKEN: TransferToken = {
|
|
|
103
188
|
* move stake) names a display `amount` of IXO, converted with IXO's 6
|
|
104
189
|
* decimals; what a stake takes from the wallet counts against the listed
|
|
105
190
|
* IXO balance together with the IXO bank steps, when the list has IXO
|
|
106
|
-
* (Auth Hub checks every balance again before the card is made).
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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`.
|
|
110
198
|
*/
|
|
111
199
|
export function chainTxAmountsInBaseUnits(
|
|
112
200
|
messages: readonly ChainTxMessageInput[],
|
|
@@ -128,17 +216,15 @@ export function chainTxAmountsInBaseUnits(
|
|
|
128
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'}.`,
|
|
129
217
|
);
|
|
130
218
|
};
|
|
131
|
-
const converted = (at: string, amount: string, decimals: number) => {
|
|
132
|
-
try {
|
|
133
|
-
return toBaseUnits(amount, decimals);
|
|
134
|
-
} catch (error) {
|
|
135
|
-
if (error instanceof HubActionRefused)
|
|
136
|
-
throw new HubActionRefused('refused', `${at}.amount: ${error.message}`);
|
|
137
|
-
throw error;
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
219
|
return messages.map((message, index): ChainTxHostMessage => {
|
|
141
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
|
+
};
|
|
142
228
|
if (
|
|
143
229
|
message.type === 'cosmos.staking.delegate' ||
|
|
144
230
|
message.type === 'cosmos.staking.undelegate' ||
|
|
@@ -231,10 +317,26 @@ export function prepareSubjectOf(
|
|
|
231
317
|
: 'operation';
|
|
232
318
|
}
|
|
233
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
|
+
|
|
234
336
|
/** What the model is told when Auth Hub refuses a prepare (400). */
|
|
235
337
|
const HUB_PREPARE_REFUSALS: Record<
|
|
236
338
|
string,
|
|
237
|
-
(subject: HubPrepareSubject) => string
|
|
339
|
+
(subject: HubPrepareSubject, steps: readonly ChainTxMessageType[]) => string
|
|
238
340
|
> = {
|
|
239
341
|
simulation_failed: (subject) =>
|
|
240
342
|
`Auth Hub simulated this ${subject} and it would fail on chain`,
|
|
@@ -247,10 +349,11 @@ const HUB_PREPARE_REFUSALS: Record<
|
|
|
247
349
|
// it or tell the owner what was wrong.
|
|
248
350
|
operation_not_registered: (subject) =>
|
|
249
351
|
`Auth Hub did not accept this ${subject} as written`,
|
|
250
|
-
// The chain's authorization
|
|
251
|
-
// the collection admin has not authorized this account
|
|
252
|
-
|
|
253
|
-
|
|
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)}`,
|
|
254
357
|
};
|
|
255
358
|
/** Appended to every refusal no retry or change of arguments can lift. */
|
|
256
359
|
const NOT_FIXABLE = 'retrying or changing the fields will not help';
|
|
@@ -342,6 +445,7 @@ export function hubPrepareRefusal(
|
|
|
342
445
|
status: number,
|
|
343
446
|
body: unknown,
|
|
344
447
|
subject: HubPrepareSubject = 'transfer',
|
|
448
|
+
steps: readonly ChainTxMessageType[] = [],
|
|
345
449
|
): HubActionRefused | null {
|
|
346
450
|
const parsed = hubErrorBody(body);
|
|
347
451
|
const unfixable = unfixableRefusal(status, parsed, subject);
|
|
@@ -376,7 +480,7 @@ export function hubPrepareRefusal(
|
|
|
376
480
|
if (words)
|
|
377
481
|
return new HubActionRefused(
|
|
378
482
|
'refused',
|
|
379
|
-
`${words(subject)}.${reason ? ` ${reason}.` : ''}`,
|
|
483
|
+
`${words(subject, steps)}.${reason ? ` ${reason}.` : ''}`,
|
|
380
484
|
undefined,
|
|
381
485
|
status,
|
|
382
486
|
);
|