@ixo/oracle-runtime-workers 0.32.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/do/user-oracle-do.ts +5 -7
- package/src/hub-actions/chain-tx-refusals.ts +93 -3
- package/src/hub-actions/client.ts +68 -34
- 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 +219 -7
- package/src/plugins/hub-actions/hub-actions.plugin.ts +188 -31
- package/src/plugins/ixo-chain/kinds/indexed/dao-proposals.ts +37 -2
- package/src/plugins/ixo-chain/kinds/indexed/daos.ts +145 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ixo/oracle-runtime-workers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
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.0",
|
|
38
38
|
"@blocknote/server-util": "0.29.1",
|
|
39
39
|
"@composio/core": "0.10.0",
|
|
40
40
|
"@composio/langchain": "0.9.2",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"@ixo/topic-protocol": "1.0.0-rc.8",
|
|
69
69
|
"survey-core": "2.3.11",
|
|
70
70
|
"@ixo/common": "1.7.1",
|
|
71
|
-
"@ixo/
|
|
72
|
-
"@ixo/
|
|
71
|
+
"@ixo/ixo-transaction": "0.2.0",
|
|
72
|
+
"@ixo/ucan": "2.3.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@cloudflare/vitest-pool-workers": "^0.22.0",
|
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,38 @@ 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 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`.
|
|
116
124
|
*/
|
|
117
|
-
const HUB_PROTOCOL_VERSION = '
|
|
118
|
-
/** The lowest version this runtime works at:
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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. */
|
|
124
136
|
export const HUB_PROTOCOL_RECHECK_MS = 10 * 60_000;
|
|
125
137
|
|
|
126
138
|
/**
|
|
127
139
|
* What the model is told when Auth Hub refuses a read (capabilities,
|
|
128
140
|
* options, status) with 400 `invalid_request`. A read carries nothing but
|
|
129
141
|
* 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.
|
|
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.
|
|
133
145
|
*/
|
|
134
|
-
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.`;
|
|
135
147
|
/** Every Hub call but a chain-transaction prepare. */
|
|
136
148
|
const HUB_TIMEOUT_MS = 10_000;
|
|
137
149
|
/**
|
|
@@ -155,7 +167,9 @@ function isUpdateRequired(error: unknown): boolean {
|
|
|
155
167
|
|
|
156
168
|
export class HubActionClient {
|
|
157
169
|
private readonly origin: string;
|
|
158
|
-
/**
|
|
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`. */
|
|
159
173
|
private fallbackUntil = 0;
|
|
160
174
|
|
|
161
175
|
constructor(private readonly transport: HubActionTransport) {
|
|
@@ -179,37 +193,53 @@ export class HubActionClient {
|
|
|
179
193
|
/** The protocol version the next call declares. */
|
|
180
194
|
get protocolVersion(): HubProtocolVersion {
|
|
181
195
|
return Date.now() < this.fallbackUntil
|
|
182
|
-
?
|
|
196
|
+
? this.fallbackVersion
|
|
183
197
|
: HUB_PROTOCOL_VERSION;
|
|
184
198
|
}
|
|
185
199
|
|
|
186
200
|
/**
|
|
187
|
-
* A read at the current version; when Auth Hub refuses
|
|
188
|
-
*
|
|
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
|
|
189
204
|
* `HUB_PROTOCOL_RECHECK_MS`. Only reads do this: a refused read changed
|
|
190
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`.
|
|
191
207
|
*/
|
|
192
208
|
private async negotiated<T>(
|
|
193
209
|
read: (version: HubProtocolVersion) => Promise<T>,
|
|
194
210
|
): Promise<T> {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|
|
204
227
|
}
|
|
228
|
+
// Unreachable: the loop returns or throws at its last version.
|
|
229
|
+
throw new Error('No Hub protocol version to read at');
|
|
205
230
|
}
|
|
206
231
|
|
|
207
|
-
/**
|
|
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
|
+
*/
|
|
208
237
|
private async send(
|
|
209
238
|
path: string,
|
|
210
239
|
body?: unknown,
|
|
211
240
|
timeoutMs = HUB_TIMEOUT_MS,
|
|
212
241
|
subject?: HubPrepareSubject,
|
|
242
|
+
steps?: readonly ChainTxMessageType[],
|
|
213
243
|
): Promise<unknown> {
|
|
214
244
|
const request = new Request(new URL(path, this.origin), {
|
|
215
245
|
method: body === undefined ? 'GET' : 'POST',
|
|
@@ -243,6 +273,7 @@ export class HubActionClient {
|
|
|
243
273
|
response.status,
|
|
244
274
|
await response.json().catch(() => null),
|
|
245
275
|
subject,
|
|
276
|
+
steps,
|
|
246
277
|
);
|
|
247
278
|
if (refusal) throw refusal;
|
|
248
279
|
}
|
|
@@ -281,6 +312,9 @@ export class HubActionClient {
|
|
|
281
312
|
? HUB_CHAIN_TX_PREPARE_TIMEOUT_MS
|
|
282
313
|
: HUB_TIMEOUT_MS,
|
|
283
314
|
prepareSubjectOf(operation.kind),
|
|
315
|
+
operation.kind === 'ixo.chain.tx.v1'
|
|
316
|
+
? operation.messages.map((message) => message.type)
|
|
317
|
+
: [],
|
|
284
318
|
);
|
|
285
319
|
if (!response || typeof response !== 'object' || !('reference' in response))
|
|
286
320
|
throw new Error('Hub action response has no reference');
|
|
@@ -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
|
);
|
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;
|