@ixo/oracle-runtime-workers 0.29.1 → 0.30.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 +3 -3
- package/src/plugins/ixo-chain/balances.ts +141 -0
- package/src/plugins/ixo-chain/blocksync.ts +11 -6
- package/src/plugins/ixo-chain/denoms.ts +84 -36
- package/src/plugins/ixo-chain/http.ts +300 -63
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +182 -301
- package/src/plugins/ixo-chain/kinds/chain/account.ts +516 -0
- package/src/plugins/ixo-chain/kinds/chain/claims.ts +287 -0
- package/src/plugins/ixo-chain/kinds/chain/economics.ts +237 -0
- package/src/plugins/ixo-chain/kinds/chain/governance.ts +455 -0
- package/src/plugins/ixo-chain/kinds/chain/grants.ts +682 -0
- package/src/plugins/ixo-chain/kinds/chain/index.ts +23 -0
- package/src/plugins/ixo-chain/kinds/chain/liquid-staking.ts +279 -0
- package/src/plugins/ixo-chain/kinds/chain/shared.ts +286 -0
- package/src/plugins/ixo-chain/kinds/chain/staking.ts +220 -0
- package/src/plugins/ixo-chain/kinds/coins.ts +112 -0
- package/src/plugins/ixo-chain/kinds/core.ts +246 -0
- package/src/plugins/ixo-chain/kinds/indexed/agent-deposits.ts +124 -0
- package/src/plugins/ixo-chain/kinds/indexed/dao-proposals.ts +379 -0
- package/src/plugins/ixo-chain/kinds/indexed/daos.ts +319 -0
- package/src/plugins/ixo-chain/kinds/indexed/disputes.ts +159 -0
- package/src/plugins/ixo-chain/kinds/indexed/evaluations.ts +172 -0
- package/src/plugins/ixo-chain/kinds/indexed/index.ts +21 -0
- package/src/plugins/ixo-chain/kinds/indexed/liquid-stake-history.ts +160 -0
- package/src/plugins/ixo-chain/kinds/indexed/member-budgets.ts +175 -0
- package/src/plugins/ixo-chain/kinds/indexed/shared.ts +211 -0
- package/src/plugins/ixo-chain/kinds/indexed/token-history.ts +205 -0
- package/src/plugins/ixo-chain/kinds/registry.ts +43 -0
- package/src/plugins/ixo-chain/kinds/types.ts +156 -0
- package/src/plugins/ixo-chain/lookup.ts +470 -0
- package/src/plugins/ixo-chain/overview.ts +1060 -0
- package/src/plugins/ixo-chain/queries.ts +170 -138
- package/src/plugins/ixo-chain/rest.ts +129 -54
- package/src/plugins/ixo-chain/summaries.ts +3 -2
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `claim_intents` and `collection_membership`: x/claims state the index
|
|
3
|
+
* does not answer, read from the chain.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { defineKind, type KindResult } from '../types';
|
|
7
|
+
import { displayCoins } from '../coins';
|
|
8
|
+
import { clip } from '../../summaries';
|
|
9
|
+
import {
|
|
10
|
+
COLLECTION_ID,
|
|
11
|
+
accountFor,
|
|
12
|
+
addressArg,
|
|
13
|
+
coinsOr,
|
|
14
|
+
coinsSchema,
|
|
15
|
+
durationText,
|
|
16
|
+
failed,
|
|
17
|
+
firstArg,
|
|
18
|
+
isPast,
|
|
19
|
+
} from './shared';
|
|
20
|
+
|
|
21
|
+
const pagination = z.object({ next_key: z.string().nullish() }).nullish();
|
|
22
|
+
const cw20Schema = z.object({ address: z.string(), amount: z.string() });
|
|
23
|
+
|
|
24
|
+
/** cw20 amounts as shown: the token contract and its base units. */
|
|
25
|
+
const cw20Of = (payments: z.infer<typeof cw20Schema>[]) =>
|
|
26
|
+
payments.slice(0, 5).map((payment) => ({
|
|
27
|
+
contract: clip(payment.address, 80),
|
|
28
|
+
amountBaseUnits: clip(payment.amount, 40),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
/** A store key for `pagination.key`: its bytes base64, escaped for a query string. */
|
|
32
|
+
const keyParam = (key: string) => encodeURIComponent(btoa(key));
|
|
33
|
+
|
|
34
|
+
/** cw20 amounts as shown, or `empty`. */
|
|
35
|
+
const cw20Or = <Empty extends string>(
|
|
36
|
+
payments: z.infer<typeof cw20Schema>[],
|
|
37
|
+
empty: Empty,
|
|
38
|
+
) => (payments.length ? cw20Of(payments) : empty);
|
|
39
|
+
|
|
40
|
+
const collectionIdArg = z
|
|
41
|
+
.string()
|
|
42
|
+
.describe('Claim collection id, e.g. "8250".');
|
|
43
|
+
|
|
44
|
+
/* ── claim intents ────────────────────────────────────────────────────── */
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Intents read per request. x/claims keys an intent
|
|
48
|
+
* `<agentAddress>/<collectionId>/<intentId>` (`IntentKeyCreate`) and lists
|
|
49
|
+
* the whole store in key order, so a page that starts at the key
|
|
50
|
+
* `<agent>/` (or `<agent>/<collection>/`) holds that agent's intents
|
|
51
|
+
* first; the first intent of another agent (or collection) ends them.
|
|
52
|
+
*/
|
|
53
|
+
const INTENT_SCAN = 200;
|
|
54
|
+
|
|
55
|
+
const intentsResponse = z.object({
|
|
56
|
+
intents: z.array(
|
|
57
|
+
z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
agent_address: z.string(),
|
|
60
|
+
collection_id: z.string(),
|
|
61
|
+
claim_id: z.string().optional(),
|
|
62
|
+
created_at: z.string().nullish(),
|
|
63
|
+
expire_at: z.string().nullish(),
|
|
64
|
+
status: z.string(),
|
|
65
|
+
amount: coinsSchema.default([]),
|
|
66
|
+
cw20_payment: z.array(cw20Schema).default([]),
|
|
67
|
+
cw1155_payment: z.array(z.unknown()).default([]),
|
|
68
|
+
member_address: z.string().optional(),
|
|
69
|
+
}),
|
|
70
|
+
),
|
|
71
|
+
pagination,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const claimIntentsKind = defineKind({
|
|
75
|
+
name: 'claim_intents',
|
|
76
|
+
summary:
|
|
77
|
+
"An agent's claim intents: payments reserved in escrow before a claim is submitted.",
|
|
78
|
+
explain: [
|
|
79
|
+
'A claim intent (x/claims) is an agent\'s declared plan to submit a claim in a collection: the payment it asks for (`coins`, bank coins or "none"; `cw20` tokens) is moved into the collection\'s escrow when the intent is created. `status`: active (waiting for the claim; `expiresAt` is the deadline), fulfilled (the claim was submitted, `claimId`; the escrow is paid out on approval or returned on rejection), expired (no claim in time; the escrow went back).',
|
|
80
|
+
'`pastExpiry: true` = still active on chain but past its deadline: the chain expires it on its next pass. `forMember` = the team member the intent is on behalf of.',
|
|
81
|
+
"The kind reads up to 200 intents starting at this account's (and collection's) first one; `matched` counts them, `complete: false` = there were more than 200, so the list may be missing some.",
|
|
82
|
+
].join('\n'),
|
|
83
|
+
source: 'chain',
|
|
84
|
+
list: true,
|
|
85
|
+
args: z.strictObject({
|
|
86
|
+
address: addressArg,
|
|
87
|
+
collectionId: collectionIdArg.optional(),
|
|
88
|
+
first: firstArg,
|
|
89
|
+
}),
|
|
90
|
+
example: {},
|
|
91
|
+
plan(input, ctx) {
|
|
92
|
+
const account = accountFor(ctx, input.address);
|
|
93
|
+
if ('invalid' in account) return account;
|
|
94
|
+
const { collectionId } = input;
|
|
95
|
+
if (collectionId !== undefined && !COLLECTION_ID.test(collectionId))
|
|
96
|
+
return { invalid: 'collectionId must be a number such as "8250".' };
|
|
97
|
+
return {
|
|
98
|
+
source: 'chain',
|
|
99
|
+
async run(): Promise<KindResult> {
|
|
100
|
+
const start = `${account.address}/${collectionId === undefined ? '' : `${collectionId}/`}`;
|
|
101
|
+
const read = await ctx.rest(
|
|
102
|
+
`/ixo/claims/intents?pagination.key=${keyParam(start)}&pagination.limit=${INTENT_SCAN}`,
|
|
103
|
+
intentsResponse,
|
|
104
|
+
{ label: 'claim intents' },
|
|
105
|
+
);
|
|
106
|
+
if (!read.ok) return failed(read.failure);
|
|
107
|
+
const { intents: page, pagination: paging } = read.value;
|
|
108
|
+
const theirs = (intent: (typeof page)[number]) =>
|
|
109
|
+
intent.agent_address === account.address &&
|
|
110
|
+
(collectionId === undefined || intent.collection_id === collectionId);
|
|
111
|
+
const end = page.findIndex((intent) => !theirs(intent));
|
|
112
|
+
const mine = end < 0 ? page : page.slice(0, end);
|
|
113
|
+
const intents = await Promise.all(
|
|
114
|
+
mine.slice(0, ctx.first).map(async (intent) => {
|
|
115
|
+
const status = intent.status.toLowerCase();
|
|
116
|
+
return {
|
|
117
|
+
id: clip(intent.id, 40),
|
|
118
|
+
collectionId: clip(intent.collection_id, 20),
|
|
119
|
+
status: clip(status, 20),
|
|
120
|
+
coins: await coinsOr(ctx, intent.amount, 'none'),
|
|
121
|
+
...(intent.cw20_payment.length
|
|
122
|
+
? { cw20: cw20Of(intent.cw20_payment) }
|
|
123
|
+
: {}),
|
|
124
|
+
...(intent.cw1155_payment.length
|
|
125
|
+
? { cw1155Payments: intent.cw1155_payment.length }
|
|
126
|
+
: {}),
|
|
127
|
+
createdAt: intent.created_at ?? null,
|
|
128
|
+
expiresAt: intent.expire_at ?? null,
|
|
129
|
+
...(status === 'active' &&
|
|
130
|
+
intent.expire_at &&
|
|
131
|
+
isPast(intent.expire_at)
|
|
132
|
+
? { pastExpiry: true }
|
|
133
|
+
: {}),
|
|
134
|
+
...(intent.claim_id
|
|
135
|
+
? { claimId: clip(intent.claim_id, 120) }
|
|
136
|
+
: {}),
|
|
137
|
+
...(intent.member_address
|
|
138
|
+
? { forMember: clip(intent.member_address, 80) }
|
|
139
|
+
: {}),
|
|
140
|
+
};
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
subject: account.subject,
|
|
146
|
+
first: ctx.first,
|
|
147
|
+
data: {
|
|
148
|
+
address: account.address,
|
|
149
|
+
...(collectionId !== undefined ? { collectionId } : {}),
|
|
150
|
+
intents,
|
|
151
|
+
matched: mine.length,
|
|
152
|
+
// Another agent's intent ended the account's, or the store did.
|
|
153
|
+
complete: end >= 0 || !paging?.next_key,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/* ── collection membership ────────────────────────────────────────────── */
|
|
162
|
+
|
|
163
|
+
const memberBudget = z.object({
|
|
164
|
+
member_address: z.string(),
|
|
165
|
+
period: z.string(),
|
|
166
|
+
period_spend_limit: coinsSchema.default([]),
|
|
167
|
+
period_spent: coinsSchema.default([]),
|
|
168
|
+
period_cw20_spend_limit: z.array(cw20Schema).default([]),
|
|
169
|
+
period_cw20_spent: z.array(cw20Schema).default([]),
|
|
170
|
+
period_reset_at: z.string().nullish(),
|
|
171
|
+
});
|
|
172
|
+
const depositBalance = z.object({
|
|
173
|
+
agent_address: z.string(),
|
|
174
|
+
amount: coinsSchema.default([]),
|
|
175
|
+
withdrawable_at: z.string().nullish(),
|
|
176
|
+
});
|
|
177
|
+
const membersResponse = z.object({
|
|
178
|
+
member_budgets: z.array(memberBudget),
|
|
179
|
+
pagination,
|
|
180
|
+
});
|
|
181
|
+
const depositsResponse = z.object({
|
|
182
|
+
balances: z.array(depositBalance),
|
|
183
|
+
pagination,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The list page that starts at `address`. x/claims keys member budgets
|
|
188
|
+
* and deposits by `<collection>/<address>` and lists one collection's
|
|
189
|
+
* under the `<collection>/` prefix, so a page keyed at the address holds
|
|
190
|
+
* that account's record first if it exists. The point routes answer HTTP
|
|
191
|
+
* 500 for an absent record, which `ctx.rest` cannot tell from a fault.
|
|
192
|
+
*/
|
|
193
|
+
const fromAddress = (address: string) =>
|
|
194
|
+
`pagination.key=${encodeURIComponent(btoa(address))}&pagination.limit=1`;
|
|
195
|
+
|
|
196
|
+
export const collectionMembershipKind = defineKind({
|
|
197
|
+
name: 'collection_membership',
|
|
198
|
+
summary:
|
|
199
|
+
"An account's team-member budget and performance deposit in one claim collection.",
|
|
200
|
+
explain: [
|
|
201
|
+
'`member: true` = the account is a team member of the collection with a spending budget (x/claims member budget): it may reserve up to `budget.spendLimit` per `budget.period` for claim intents (`budget.spent` so far this period, "none" = nothing yet; reset at `budget.resetAt`; `resetDue` = that time has passed and the budget refills on the next use). "none allowed" = the budget allows no spending of that kind (bank coins, or cw20 tokens: `cw20SpendLimit`, `cw20Spent`). `member: false` = no member budget here (an agent may still submit claims through an authz grant: see `authz_grants`).',
|
|
202
|
+
"`deposit` = the agent's performance deposit held in the collection's escrow (null = none), as `coin` (or `coins` when it holds several denoms): it can be slashed when a dispute against the agent is lost. `lockEnded: true` = its lock (`withdrawableAt`, null = none) is over, so it can be withdrawn unless a dispute against the agent is still open.",
|
|
203
|
+
'Amounts are display units (`amount` + `symbol`) with `amountBaseUnits`; cw20 amounts are base units of the token contract.',
|
|
204
|
+
].join('\n'),
|
|
205
|
+
source: 'chain',
|
|
206
|
+
list: false,
|
|
207
|
+
args: z.strictObject({ collectionId: collectionIdArg, address: addressArg }),
|
|
208
|
+
example: { collectionId: '8250' },
|
|
209
|
+
plan(input, ctx) {
|
|
210
|
+
const account = accountFor(ctx, input.address);
|
|
211
|
+
if ('invalid' in account) return account;
|
|
212
|
+
const { collectionId } = input;
|
|
213
|
+
if (!COLLECTION_ID.test(collectionId))
|
|
214
|
+
return { invalid: 'collectionId must be a number such as "8250".' };
|
|
215
|
+
const base = `/ixo/claims/collection/${collectionId}`;
|
|
216
|
+
return {
|
|
217
|
+
source: 'chain',
|
|
218
|
+
async run(): Promise<KindResult> {
|
|
219
|
+
const [members, deposits] = await Promise.all([
|
|
220
|
+
ctx.rest(
|
|
221
|
+
`${base}/members?${fromAddress(account.address)}`,
|
|
222
|
+
membersResponse,
|
|
223
|
+
{ label: 'collection member budget' },
|
|
224
|
+
),
|
|
225
|
+
ctx.rest(
|
|
226
|
+
`${base}/deposits?${fromAddress(account.address)}`,
|
|
227
|
+
depositsResponse,
|
|
228
|
+
{ label: 'collection deposit' },
|
|
229
|
+
),
|
|
230
|
+
]);
|
|
231
|
+
if (!members.ok) return failed(members.failure);
|
|
232
|
+
if (!deposits.ok) return failed(deposits.failure);
|
|
233
|
+
const budget = members.value.member_budgets.find(
|
|
234
|
+
(entry) => entry.member_address === account.address,
|
|
235
|
+
);
|
|
236
|
+
const deposit = deposits.value.balances.find(
|
|
237
|
+
(entry) => entry.agent_address === account.address,
|
|
238
|
+
);
|
|
239
|
+
const withdrawableAt = deposit?.withdrawable_at ?? null;
|
|
240
|
+
// A drained deposit is removed from the store: an empty one is none.
|
|
241
|
+
const held = deposit ? await displayCoins(ctx, deposit.amount) : [];
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
subject: account.subject,
|
|
245
|
+
data: {
|
|
246
|
+
address: account.address,
|
|
247
|
+
collectionId,
|
|
248
|
+
member: !!budget,
|
|
249
|
+
...(budget
|
|
250
|
+
? {
|
|
251
|
+
budget: {
|
|
252
|
+
period: durationText(budget.period),
|
|
253
|
+
// The intent handler lets a member spend only what the
|
|
254
|
+
// limit lists: an empty limit allows nothing.
|
|
255
|
+
spendLimit: await coinsOr(
|
|
256
|
+
ctx,
|
|
257
|
+
budget.period_spend_limit,
|
|
258
|
+
'none allowed',
|
|
259
|
+
),
|
|
260
|
+
spent: await coinsOr(ctx, budget.period_spent, 'none'),
|
|
261
|
+
cw20SpendLimit: cw20Or(
|
|
262
|
+
budget.period_cw20_spend_limit,
|
|
263
|
+
'none allowed',
|
|
264
|
+
),
|
|
265
|
+
cw20Spent: cw20Or(budget.period_cw20_spent, 'none'),
|
|
266
|
+
resetAt: budget.period_reset_at ?? null,
|
|
267
|
+
...(budget.period_reset_at && isPast(budget.period_reset_at)
|
|
268
|
+
? { resetDue: true }
|
|
269
|
+
: {}),
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
: {}),
|
|
273
|
+
deposit: held.length
|
|
274
|
+
? {
|
|
275
|
+
...(held.length === 1 && held[0]
|
|
276
|
+
? { coin: held[0] }
|
|
277
|
+
: { coins: held }),
|
|
278
|
+
withdrawableAt,
|
|
279
|
+
lockEnded: withdrawableAt === null || isPast(withdrawableAt),
|
|
280
|
+
}
|
|
281
|
+
: null,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `network_economics`: how new IXO is minted. The x/mint parameters, the
|
|
3
|
+
* provisions the chain mints this epoch and the epoch clock, each from a
|
|
4
|
+
* 10 min per-isolate cache.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { defineKind, type KindContext, type KindResult } from '../types';
|
|
8
|
+
import { displayCoin } from '../coins';
|
|
9
|
+
import { clip } from '../../summaries';
|
|
10
|
+
import {
|
|
11
|
+
DEC_ONE,
|
|
12
|
+
ReadCache,
|
|
13
|
+
decMul,
|
|
14
|
+
decPercent,
|
|
15
|
+
durationText,
|
|
16
|
+
failed,
|
|
17
|
+
parseDec,
|
|
18
|
+
sharePercent,
|
|
19
|
+
} from './shared';
|
|
20
|
+
|
|
21
|
+
/** Mint parameters, provisions and epochs are reused for 10 min. */
|
|
22
|
+
export const ECONOMICS_TTL_MS = 600_000;
|
|
23
|
+
/** Reductions replayed at most when checking the schedule (one per period; mainnet has had one). */
|
|
24
|
+
const MAX_REDUCTIONS = 1_000n;
|
|
25
|
+
|
|
26
|
+
const paramsResponse = z.object({
|
|
27
|
+
params: z.object({
|
|
28
|
+
mint_denom: z.string(),
|
|
29
|
+
genesis_epoch_provisions: z.string(),
|
|
30
|
+
epoch_identifier: z.string(),
|
|
31
|
+
reduction_period_in_epochs: z.string(),
|
|
32
|
+
reduction_factor: z.string(),
|
|
33
|
+
distribution_proportions: z.object({
|
|
34
|
+
staking: z.string(),
|
|
35
|
+
impact_rewards: z.string(),
|
|
36
|
+
community_pool: z.string(),
|
|
37
|
+
}),
|
|
38
|
+
minting_rewards_distribution_start_epoch: z.string(),
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
const provisionsResponse = z.object({ epoch_provisions: z.string() });
|
|
42
|
+
const epochsResponse = z.object({
|
|
43
|
+
epochs: z.array(
|
|
44
|
+
z.object({
|
|
45
|
+
identifier: z.string(),
|
|
46
|
+
duration: z.string(),
|
|
47
|
+
current_epoch: z.string(),
|
|
48
|
+
current_epoch_start_time: z.string(),
|
|
49
|
+
epoch_counting_started: z.boolean(),
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
type Params = z.infer<typeof paramsResponse>['params'];
|
|
55
|
+
type Epoch = z.infer<typeof epochsResponse>['epochs'][number];
|
|
56
|
+
|
|
57
|
+
const params = new ReadCache<z.infer<typeof paramsResponse>>(
|
|
58
|
+
ECONOMICS_TTL_MS,
|
|
59
|
+
8,
|
|
60
|
+
);
|
|
61
|
+
const provisions = new ReadCache<z.infer<typeof provisionsResponse>>(
|
|
62
|
+
ECONOMICS_TTL_MS,
|
|
63
|
+
8,
|
|
64
|
+
);
|
|
65
|
+
const epochs = new ReadCache<z.infer<typeof epochsResponse>>(
|
|
66
|
+
ECONOMICS_TTL_MS,
|
|
67
|
+
8,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const cached = <S extends z.ZodType>(
|
|
71
|
+
cache: ReadCache<z.infer<S>>,
|
|
72
|
+
ctx: KindContext,
|
|
73
|
+
path: string,
|
|
74
|
+
schema: S,
|
|
75
|
+
label: string,
|
|
76
|
+
) => cache.get(ctx, path, () => ctx.rest(path, schema, { label }));
|
|
77
|
+
|
|
78
|
+
const COUNT = /^\d{1,12}$/;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The next provisions reduction. x/mint cuts the provisions by
|
|
82
|
+
* `reduction_factor` at the end of the epoch `start + n × period`
|
|
83
|
+
* (`start` = the distribution start epoch); the chain does not expose
|
|
84
|
+
* the epoch of the last cut, so the schedule is replayed from the
|
|
85
|
+
* genesis provisions with the chain's own decimal rounding and used only
|
|
86
|
+
* when it reproduces exactly the provisions the chain reports now (a
|
|
87
|
+
* parameter changed by governance on the way would not). Null otherwise,
|
|
88
|
+
* or before minting started.
|
|
89
|
+
*/
|
|
90
|
+
function nextReduction(
|
|
91
|
+
values: Params,
|
|
92
|
+
current: bigint,
|
|
93
|
+
epoch: Epoch,
|
|
94
|
+
): { epoch: string; around: string | null; provisionsAfter: bigint } | null {
|
|
95
|
+
const genesis = parseDec(values.genesis_epoch_provisions);
|
|
96
|
+
const factor = parseDec(values.reduction_factor);
|
|
97
|
+
const count = (value: string) => (COUNT.test(value) ? BigInt(value) : null);
|
|
98
|
+
const start = count(values.minting_rewards_distribution_start_epoch);
|
|
99
|
+
const period = count(values.reduction_period_in_epochs);
|
|
100
|
+
const now = count(epoch.current_epoch);
|
|
101
|
+
if (
|
|
102
|
+
genesis === null ||
|
|
103
|
+
factor === null ||
|
|
104
|
+
start === null ||
|
|
105
|
+
period === null ||
|
|
106
|
+
now === null
|
|
107
|
+
)
|
|
108
|
+
return null;
|
|
109
|
+
// The epoch in progress has not ended: the last one ended is now − 1.
|
|
110
|
+
const lastEnded = now - 1n;
|
|
111
|
+
if (period <= 0n || !epoch.epoch_counting_started || lastEnded < start)
|
|
112
|
+
return null;
|
|
113
|
+
const cuts = (lastEnded - start) / period;
|
|
114
|
+
if (cuts > MAX_REDUCTIONS) return null;
|
|
115
|
+
let expected = genesis;
|
|
116
|
+
for (let n = 0n; n < cuts; n += 1n) expected = decMul(expected, factor);
|
|
117
|
+
if (expected !== current) return null;
|
|
118
|
+
const target = start + (cuts + 1n) * period;
|
|
119
|
+
const seconds = /^(\d{1,12})s$/.exec(epoch.duration)?.[1];
|
|
120
|
+
const startedAt = Date.parse(epoch.current_epoch_start_time);
|
|
121
|
+
// The reduction runs when the target epoch ends.
|
|
122
|
+
const around =
|
|
123
|
+
seconds && Number.isFinite(startedAt)
|
|
124
|
+
? new Date(
|
|
125
|
+
startedAt + Number((target - now + 1n) * BigInt(seconds)) * 1000,
|
|
126
|
+
).toISOString()
|
|
127
|
+
: null;
|
|
128
|
+
return {
|
|
129
|
+
epoch: target.toString(),
|
|
130
|
+
around,
|
|
131
|
+
provisionsAfter: decMul(current, factor),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const networkEconomicsKind = defineKind({
|
|
136
|
+
name: 'network_economics',
|
|
137
|
+
summary:
|
|
138
|
+
'How new IXO is minted: provisions per epoch, the next reduction, where it goes.',
|
|
139
|
+
explain: [
|
|
140
|
+
'IXO inflation (x/mint): at the end of every epoch (`epoch.identifier`, e.g. "day", each `epoch.length`) the chain mints `provisionsPerEpoch` new IXO and splits it by `distribution` (staking rewards to delegators, impact rewards, the community pool).',
|
|
141
|
+
'Every `reduction.everyEpochs` epochs the provisions are multiplied by `reduction.factor` (a cut of `reduction.cutPercent`). `reduction.next` gives the epoch of the next cut, roughly when (`around`, the end of that epoch) and the provisions after it; it is left out when the current parameters do not reproduce the provisions the chain reports (e.g. after a governance change), so it is never a guess.',
|
|
142
|
+
'Amounts are display units (`amount` + `symbol`) with `amountBaseUnits` (fractions of a base unit dropped).',
|
|
143
|
+
].join('\n'),
|
|
144
|
+
source: 'chain',
|
|
145
|
+
list: false,
|
|
146
|
+
args: z.strictObject({}),
|
|
147
|
+
example: {},
|
|
148
|
+
plan(_input, ctx) {
|
|
149
|
+
return {
|
|
150
|
+
source: 'chain',
|
|
151
|
+
async run(): Promise<KindResult> {
|
|
152
|
+
const [paramsRead, provisionsRead, epochsRead] = await Promise.all([
|
|
153
|
+
cached(
|
|
154
|
+
params,
|
|
155
|
+
ctx,
|
|
156
|
+
'/ixo/mint/v1beta1/params',
|
|
157
|
+
paramsResponse,
|
|
158
|
+
'mint params',
|
|
159
|
+
),
|
|
160
|
+
cached(
|
|
161
|
+
provisions,
|
|
162
|
+
ctx,
|
|
163
|
+
'/ixo/mint/v1beta1/epoch_provisions',
|
|
164
|
+
provisionsResponse,
|
|
165
|
+
'epoch provisions',
|
|
166
|
+
),
|
|
167
|
+
cached(
|
|
168
|
+
epochs,
|
|
169
|
+
ctx,
|
|
170
|
+
'/ixo/epochs/v1beta1/epochs',
|
|
171
|
+
epochsResponse,
|
|
172
|
+
'epochs',
|
|
173
|
+
),
|
|
174
|
+
]);
|
|
175
|
+
if (!paramsRead.ok) return failed(paramsRead.failure);
|
|
176
|
+
if (!provisionsRead.ok) return failed(provisionsRead.failure);
|
|
177
|
+
if (!epochsRead.ok) return failed(epochsRead.failure);
|
|
178
|
+
const values = paramsRead.value.params;
|
|
179
|
+
const denom = values.mint_denom;
|
|
180
|
+
const current = parseDec(provisionsRead.value.epoch_provisions);
|
|
181
|
+
const epoch = epochsRead.value.epochs.find(
|
|
182
|
+
(entry) => entry.identifier === values.epoch_identifier,
|
|
183
|
+
);
|
|
184
|
+
const coin = (scaled: bigint) =>
|
|
185
|
+
displayCoin(ctx, { denom, amount: (scaled / DEC_ONE).toString() });
|
|
186
|
+
const next =
|
|
187
|
+
current !== null && epoch
|
|
188
|
+
? nextReduction(values, current, epoch)
|
|
189
|
+
: null;
|
|
190
|
+
const factor = parseDec(values.reduction_factor);
|
|
191
|
+
const split = values.distribution_proportions;
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
data: {
|
|
195
|
+
mintDenom: clip(denom, 60),
|
|
196
|
+
...(epoch
|
|
197
|
+
? {
|
|
198
|
+
epoch: {
|
|
199
|
+
identifier: clip(epoch.identifier, 40),
|
|
200
|
+
length: durationText(epoch.duration),
|
|
201
|
+
current: epoch.current_epoch,
|
|
202
|
+
startedAt: epoch.current_epoch_start_time,
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
: {}),
|
|
206
|
+
...(current !== null
|
|
207
|
+
? { provisionsPerEpoch: await coin(current) }
|
|
208
|
+
: {}),
|
|
209
|
+
reduction: {
|
|
210
|
+
factor: clip(values.reduction_factor, 30),
|
|
211
|
+
...(factor !== null && factor <= DEC_ONE
|
|
212
|
+
? { cutPercent: sharePercent(DEC_ONE - factor, DEC_ONE) }
|
|
213
|
+
: {}),
|
|
214
|
+
everyEpochs: values.reduction_period_in_epochs,
|
|
215
|
+
...(next
|
|
216
|
+
? {
|
|
217
|
+
next: {
|
|
218
|
+
epoch: next.epoch,
|
|
219
|
+
around: next.around,
|
|
220
|
+
provisionsAfter: await coin(next.provisionsAfter),
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
: {}),
|
|
224
|
+
},
|
|
225
|
+
distribution: {
|
|
226
|
+
staking: decPercent(split.staking),
|
|
227
|
+
impactRewards: decPercent(split.impact_rewards),
|
|
228
|
+
communityPool: decPercent(split.community_pool),
|
|
229
|
+
},
|
|
230
|
+
distributionStartEpoch:
|
|
231
|
+
values.minting_rewards_distribution_start_epoch,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
});
|