@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.
Files changed (34) hide show
  1. package/package.json +3 -3
  2. package/src/plugins/ixo-chain/balances.ts +141 -0
  3. package/src/plugins/ixo-chain/blocksync.ts +11 -6
  4. package/src/plugins/ixo-chain/denoms.ts +84 -36
  5. package/src/plugins/ixo-chain/http.ts +300 -63
  6. package/src/plugins/ixo-chain/ixo-chain.plugin.ts +182 -301
  7. package/src/plugins/ixo-chain/kinds/chain/account.ts +516 -0
  8. package/src/plugins/ixo-chain/kinds/chain/claims.ts +287 -0
  9. package/src/plugins/ixo-chain/kinds/chain/economics.ts +237 -0
  10. package/src/plugins/ixo-chain/kinds/chain/governance.ts +455 -0
  11. package/src/plugins/ixo-chain/kinds/chain/grants.ts +682 -0
  12. package/src/plugins/ixo-chain/kinds/chain/index.ts +23 -0
  13. package/src/plugins/ixo-chain/kinds/chain/liquid-staking.ts +279 -0
  14. package/src/plugins/ixo-chain/kinds/chain/shared.ts +286 -0
  15. package/src/plugins/ixo-chain/kinds/chain/staking.ts +220 -0
  16. package/src/plugins/ixo-chain/kinds/coins.ts +112 -0
  17. package/src/plugins/ixo-chain/kinds/core.ts +246 -0
  18. package/src/plugins/ixo-chain/kinds/indexed/agent-deposits.ts +124 -0
  19. package/src/plugins/ixo-chain/kinds/indexed/dao-proposals.ts +379 -0
  20. package/src/plugins/ixo-chain/kinds/indexed/daos.ts +319 -0
  21. package/src/plugins/ixo-chain/kinds/indexed/disputes.ts +159 -0
  22. package/src/plugins/ixo-chain/kinds/indexed/evaluations.ts +172 -0
  23. package/src/plugins/ixo-chain/kinds/indexed/index.ts +21 -0
  24. package/src/plugins/ixo-chain/kinds/indexed/liquid-stake-history.ts +160 -0
  25. package/src/plugins/ixo-chain/kinds/indexed/member-budgets.ts +175 -0
  26. package/src/plugins/ixo-chain/kinds/indexed/shared.ts +211 -0
  27. package/src/plugins/ixo-chain/kinds/indexed/token-history.ts +205 -0
  28. package/src/plugins/ixo-chain/kinds/registry.ts +43 -0
  29. package/src/plugins/ixo-chain/kinds/types.ts +156 -0
  30. package/src/plugins/ixo-chain/lookup.ts +470 -0
  31. package/src/plugins/ixo-chain/overview.ts +1060 -0
  32. package/src/plugins/ixo-chain/queries.ts +170 -138
  33. package/src/plugins/ixo-chain/rest.ts +129 -54
  34. package/src/plugins/ixo-chain/summaries.ts +3 -2
@@ -0,0 +1,516 @@
1
+ /**
2
+ * `account`, `smart_account` and `spendable`: what kind of account an
3
+ * address is, the extra signing rules (authenticators) on it, and how
4
+ * much of its balance it can spend now.
5
+ */
6
+ import { z } from 'zod';
7
+ import { defineKind, type KindContext, type KindResult } from '../types';
8
+ import { coinView, displaysOf, wholeUnits } from '../coins';
9
+ import { clip } from '../../summaries';
10
+ import {
11
+ accountFor,
12
+ addressArg,
13
+ coinsOr,
14
+ coinsSchema,
15
+ failed,
16
+ shortType,
17
+ } from './shared';
18
+
19
+ const args = z.strictObject({ address: addressArg });
20
+
21
+ const anyTyped = z.looseObject({ '@type': z.string() });
22
+
23
+ /* ── account ──────────────────────────────────────────────────────────── */
24
+
25
+ const accountResponse = z.object({ account: anyTyped });
26
+ const baseAccount = z.object({
27
+ address: z.string(),
28
+ pub_key: anyTyped.nullish(),
29
+ account_number: z.string(),
30
+ sequence: z.string(),
31
+ });
32
+ const moduleAccount = z.object({
33
+ base_account: baseAccount,
34
+ name: z.string(),
35
+ permissions: z.array(z.string()).default([]),
36
+ });
37
+ const baseVesting = z.object({
38
+ base_account: baseAccount,
39
+ original_vesting: coinsSchema.default([]),
40
+ end_time: z.string(),
41
+ });
42
+ const vestingAccount = z.object({
43
+ base_vesting_account: baseVesting,
44
+ start_time: z.string().optional(),
45
+ vesting_periods: z
46
+ .array(z.object({ length: z.string(), amount: coinsSchema }))
47
+ .optional(),
48
+ });
49
+ const contractResponse = z.object({
50
+ contract_info: z.object({
51
+ code_id: z.string(),
52
+ creator: z.string(),
53
+ admin: z.string().optional(),
54
+ label: z.string(),
55
+ }),
56
+ });
57
+
58
+ /**
59
+ * A 32-byte account address (`ixo1` and 58 characters): the length of a
60
+ * CosmWasm contract (and of module-derived accounts), as against a key's
61
+ * 20 bytes (42 characters).
62
+ */
63
+ const LONG_ADDRESS_CHARS = 62;
64
+
65
+ const VESTING_KIND: Record<string, string> = {
66
+ '/cosmos.vesting.v1beta1.ContinuousVestingAccount': 'continuous',
67
+ '/cosmos.vesting.v1beta1.DelayedVestingAccount': 'delayed',
68
+ '/cosmos.vesting.v1beta1.PeriodicVestingAccount': 'periodic',
69
+ '/cosmos.vesting.v1beta1.PermanentLockedAccount': 'permanently locked',
70
+ };
71
+
72
+ const PUBLIC_KEY: Record<string, string> = {
73
+ '/cosmos.crypto.secp256k1.PubKey': 'secp256k1',
74
+ '/cosmos.crypto.secp256r1.PubKey': 'secp256r1',
75
+ '/cosmos.crypto.ed25519.PubKey': 'ed25519',
76
+ '/cosmos.crypto.multisig.LegacyAminoPubKey': 'multisig',
77
+ };
78
+
79
+ /** Unix seconds (as the chain's JSON gives them) as an ISO time; null when not a time. */
80
+ function unixTime(seconds: string): string | null {
81
+ if (!/^\d{1,12}$/.test(seconds)) return null;
82
+ return new Date(Number(seconds) * 1000).toISOString();
83
+ }
84
+
85
+ function baseView(base: z.infer<typeof baseAccount>) {
86
+ const key = base.pub_key?.['@type'];
87
+ return {
88
+ accountNumber: base.account_number,
89
+ sequence: base.sequence,
90
+ publicKey: key ? (PUBLIC_KEY[key] ?? shortType(key)) : 'none yet',
91
+ };
92
+ }
93
+
94
+ /** A vesting account's schedule in words and times. */
95
+ async function vestingView(
96
+ ctx: KindContext,
97
+ kind: string,
98
+ account: z.infer<typeof vestingAccount>,
99
+ ) {
100
+ const base = account.base_vesting_account;
101
+ const start = account.start_time ? unixTime(account.start_time) : null;
102
+ const end = unixTime(base.end_time);
103
+ return {
104
+ kind,
105
+ coins: await coinsOr(ctx, base.original_vesting, 'none'),
106
+ ...(kind === 'continuous' || kind === 'periodic'
107
+ ? { starts: start, ends: end }
108
+ : {}),
109
+ ...(kind === 'delayed' ? { unlocksAt: end } : {}),
110
+ ...(account.vesting_periods
111
+ ? { periods: account.vesting_periods.length }
112
+ : {}),
113
+ };
114
+ }
115
+
116
+ async function accountView(
117
+ ctx: KindContext,
118
+ account: z.infer<typeof anyTyped>,
119
+ contract: z.infer<typeof contractResponse> | null,
120
+ ): Promise<Record<string, unknown>> {
121
+ const typeUrl = account['@type'];
122
+ const type = shortType(typeUrl);
123
+ if (typeUrl === '/cosmos.auth.v1beta1.BaseAccount') {
124
+ const parsed = baseAccount.safeParse(account);
125
+ if (parsed.success) {
126
+ const info = contract?.contract_info;
127
+ // The label is the contract creator's own text: it stays in
128
+ // `contract.label`, never in the label this reader writes.
129
+ return {
130
+ type,
131
+ kindLabel: info ? 'smart contract' : 'regular account (held by a key)',
132
+ ...baseView(parsed.data),
133
+ ...(info
134
+ ? {
135
+ contract: {
136
+ label: clip(info.label, 120),
137
+ codeId: info.code_id,
138
+ creator: info.creator,
139
+ admin: info.admin || null,
140
+ },
141
+ }
142
+ : {}),
143
+ };
144
+ }
145
+ }
146
+ if (typeUrl === '/cosmos.auth.v1beta1.ModuleAccount') {
147
+ const parsed = moduleAccount.safeParse(account);
148
+ if (parsed.success)
149
+ return {
150
+ type,
151
+ kindLabel: `module account "${clip(parsed.data.name, 60)}"`,
152
+ ...baseView(parsed.data.base_account),
153
+ module: {
154
+ name: clip(parsed.data.name, 60),
155
+ permissions: parsed.data.permissions.slice(0, 10),
156
+ },
157
+ };
158
+ }
159
+ const vestingKind = VESTING_KIND[typeUrl];
160
+ if (vestingKind) {
161
+ const parsed = vestingAccount.safeParse(account);
162
+ if (parsed.success)
163
+ return {
164
+ type,
165
+ kindLabel: `vesting account (${vestingKind})`,
166
+ ...baseView(parsed.data.base_vesting_account.base_account),
167
+ vesting: await vestingView(ctx, vestingKind, parsed.data),
168
+ };
169
+ }
170
+ return { type, kindLabel: 'an account type this reader does not know' };
171
+ }
172
+
173
+ export const accountKind = defineKind({
174
+ name: 'account',
175
+ summary:
176
+ 'What an address is on chain: regular, module, vesting or contract account.',
177
+ explain: [
178
+ '`onChain: false` = the chain has no account at this address yet: nothing was ever sent to it and it never signed. That is not an error.',
179
+ '`kindLabel`: regular account (held by a key: a person or a service), module account "<name>" (owned by chain code, e.g. fee_collector; `module.permissions`), vesting account (continuous / delayed / periodic / permanently locked: `vesting.coins` unlock over `starts`–`ends`, at `unlocksAt`, or never; see `spendable` for what is locked now), smart contract (CosmWasm: `contract.codeId`, `creator`, `admin` who may migrate it, null = nobody; `contract.label` is chosen by whoever instantiated it, so it proves nothing about who runs it).',
180
+ '`accountNumber` is fixed when the account is created. `sequence` = how many transactions the account has signed (a signal of activity). `publicKey`: the key type the account signs with (secp256k1 for wallets); "none yet" = it has never signed (and a contract or module account never does).',
181
+ ].join('\n'),
182
+ source: 'chain',
183
+ list: false,
184
+ args,
185
+ example: {},
186
+ plan(input, ctx) {
187
+ const account = accountFor(ctx, input.address);
188
+ if ('invalid' in account) return account;
189
+ return {
190
+ source: 'chain',
191
+ async run(): Promise<KindResult> {
192
+ const [read, contract] = await Promise.all([
193
+ ctx.rest(
194
+ `/cosmos/auth/v1beta1/accounts/${account.address}`,
195
+ accountResponse,
196
+ { label: 'account', notFoundOn404: true },
197
+ ),
198
+ account.address.length === LONG_ADDRESS_CHARS
199
+ ? ctx.rest(
200
+ `/cosmwasm/wasm/v1/contract/${account.address}`,
201
+ contractResponse,
202
+ { label: 'contract info', notFoundOn404: true },
203
+ )
204
+ : null,
205
+ ]);
206
+ if (!read.ok) {
207
+ if (read.failure.reason !== 'not_found') return failed(read.failure);
208
+ return {
209
+ ok: true,
210
+ subject: account.subject,
211
+ data: { address: account.address, onChain: false },
212
+ };
213
+ }
214
+ // A contract read that failed for another reason than "not a
215
+ // contract" leaves the account answer standing, marked.
216
+ const contractUnread =
217
+ contract && !contract.ok && contract.failure.reason !== 'not_found';
218
+ return {
219
+ ok: true,
220
+ subject: account.subject,
221
+ data: {
222
+ address: account.address,
223
+ onChain: true,
224
+ ...(await accountView(
225
+ ctx,
226
+ read.value.account,
227
+ contract?.ok ? contract.value : null,
228
+ )),
229
+ ...(contractUnread ? { contractInfoUnavailable: true } : {}),
230
+ },
231
+ };
232
+ },
233
+ };
234
+ },
235
+ });
236
+
237
+ /* ── smart account ────────────────────────────────────────────────────── */
238
+
239
+ const authenticatorsResponse = z.object({
240
+ account_authenticators: z
241
+ .array(z.object({ id: z.string(), type: z.string(), config: z.string() }))
242
+ .default([]),
243
+ });
244
+ /** A composite authenticator's config: its sub-authenticators, each config base64. */
245
+ const subAuthenticators = z.array(
246
+ z.object({ type: z.string(), config: z.string().nullish() }),
247
+ );
248
+
249
+ /** Authenticators listed per account; the rest are counted. */
250
+ const AUTHENTICATOR_CAP = 10;
251
+ /** Nesting read inside one authenticator, and nodes shown per authenticator. */
252
+ const RULE_DEPTH = 4;
253
+ const RULE_NODES = 16;
254
+ const RULE_CHARS = 300;
255
+ /** An authenticator type as the chain names them. */
256
+ const TYPE_NAME = /^[A-Za-z0-9]{1,40}$/;
257
+ const FIELD_NAME = /^[A-Za-z0-9_@.]{1,40}$/;
258
+
259
+ const TYPE_LABEL: Record<string, string> = {
260
+ SignatureVerification: 'signs with an extra key',
261
+ AuthnVerification: 'signs with a passkey (WebAuthn)',
262
+ AllOf: 'every listed rule must pass',
263
+ AnyOf: 'any one listed rule is enough',
264
+ PartitionedAllOf: 'every listed rule must pass, each with its own signature',
265
+ PartitionedAnyOf: 'any one listed rule, each with its own signature',
266
+ MessageFilter: 'only allows matching messages',
267
+ CosmwasmAuthenticatorV1: 'a rule run by a smart contract',
268
+ };
269
+ const COMPOSITE = new Set([
270
+ 'AllOf',
271
+ 'AnyOf',
272
+ 'PartitionedAllOf',
273
+ 'PartitionedAnyOf',
274
+ ]);
275
+
276
+ function bytesOf(base64: string): Uint8Array | null {
277
+ try {
278
+ return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+
284
+ function jsonOf(bytes: Uint8Array | null): unknown {
285
+ if (!bytes) return undefined;
286
+ try {
287
+ return JSON.parse(new TextDecoder().decode(bytes));
288
+ } catch {
289
+ return undefined;
290
+ }
291
+ }
292
+
293
+ /** The first 8 hex digits of the SHA-256 of a key: names a key without showing it. */
294
+ async function fingerprint(bytes: Uint8Array): Promise<string> {
295
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
296
+ return [...digest.slice(0, 4)]
297
+ .map((byte) => byte.toString(16).padStart(2, '0'))
298
+ .join('');
299
+ }
300
+
301
+ const messageFilter = z.looseObject({ '@type': z.string().optional() });
302
+ const cosmwasmConfig = z.looseObject({ contract: z.string() });
303
+
304
+ /**
305
+ * One authenticator as its tree of types, e.g.
306
+ * `AllOf[SignatureVerification(key 1a2b3c4d), MessageFilter(MsgSend)]`.
307
+ * Keys appear only as fingerprints; a config that does not decode is
308
+ * named so. At most {@link RULE_NODES} nodes, {@link RULE_DEPTH} deep.
309
+ */
310
+ async function ruleOf(
311
+ type: string,
312
+ config: string | null | undefined,
313
+ budget: { nodes: number },
314
+ depth: number,
315
+ ): Promise<string> {
316
+ budget.nodes -= 1;
317
+ if (budget.nodes < 0) return '…';
318
+ if (!TYPE_NAME.test(type)) return 'unknown';
319
+ const bytes = config ? bytesOf(config) : null;
320
+ if (type === 'SignatureVerification' || type === 'AuthnVerification')
321
+ return bytes?.length ? `${type}(key ${await fingerprint(bytes)})` : type;
322
+ if (type === 'MessageFilter') {
323
+ const filter = messageFilter.safeParse(jsonOf(bytes));
324
+ if (!filter.success) return `${type}(unreadable config)`;
325
+ const message = filter.data['@type'];
326
+ const fields = Object.keys(filter.data)
327
+ .filter((key) => key !== '@type' && FIELD_NAME.test(key))
328
+ .slice(0, 3);
329
+ const parts = [
330
+ message ? shortType(message) : 'any message',
331
+ ...(fields.length ? [`fields ${fields.join(', ')}`] : []),
332
+ ];
333
+ return `${type}(${parts.join('; ')})`;
334
+ }
335
+ if (type === 'CosmwasmAuthenticatorV1') {
336
+ const parsed = cosmwasmConfig.safeParse(jsonOf(bytes));
337
+ return parsed.success
338
+ ? `${type}(contract ${clip(parsed.data.contract, 70)})`
339
+ : `${type}(unreadable config)`;
340
+ }
341
+ if (COMPOSITE.has(type)) {
342
+ if (depth >= RULE_DEPTH) return `${type}[…]`;
343
+ const subs = subAuthenticators.safeParse(jsonOf(bytes));
344
+ if (!subs.success) return `${type}(unreadable config)`;
345
+ const parts: string[] = [];
346
+ for (const sub of subs.data) {
347
+ // Once the node budget is spent, the remaining sub-rules are one "…".
348
+ if (budget.nodes <= 0) {
349
+ parts.push('…');
350
+ break;
351
+ }
352
+ parts.push(await ruleOf(sub.type, sub.config, budget, depth + 1));
353
+ }
354
+ return `${type}[${parts.join(', ')}]`;
355
+ }
356
+ return type;
357
+ }
358
+
359
+ export const smartAccountKind = defineKind({
360
+ name: 'smart_account',
361
+ summary:
362
+ 'Extra signing rules (authenticators) on an account: passkeys, extra keys, message limits.',
363
+ explain: [
364
+ "A smart account (x/smart-account) has authenticators: extra rules that may authorise its transactions besides its own key, such as a passkey, a second key, or a key limited to certain messages. `smartAccount: false` = none: only the account's own key signs.",
365
+ 'Each authenticator has an `id`, its top `type` with a `kindLabel`, and `rule`: the whole tree of types. SignatureVerification(key …) = a secp256k1 key (shown only as an 8-digit fingerprint of its public key), AuthnVerification = a passkey, MessageFilter(MsgX) = only that message type (with the fields it pins), AllOf[…] = every sub-rule must pass, AnyOf[…] = any one is enough (Partitioned… = each sub-rule checks its own signature), CosmwasmAuthenticatorV1 = a contract decides.',
366
+ 'Example: AllOf[SignatureVerification(key 1a2b3c4d), AnyOf[MessageFilter(MsgAddAuthenticator), MessageFilter(MsgRemoveAuthenticator)]] = that key may only add or remove authenticators. Anyone controlling an authenticator can act as the account within its rule.',
367
+ ].join('\n'),
368
+ source: 'chain',
369
+ list: false,
370
+ args,
371
+ example: {},
372
+ plan(input, ctx) {
373
+ const account = accountFor(ctx, input.address);
374
+ if ('invalid' in account) return account;
375
+ return {
376
+ source: 'chain',
377
+ async run(): Promise<KindResult> {
378
+ const read = await ctx.rest(
379
+ `/ixo/smartaccount/authenticators/${account.address}`,
380
+ authenticatorsResponse,
381
+ { label: 'authenticators', notFoundOn404: true },
382
+ );
383
+ if (!read.ok && read.failure.reason !== 'not_found')
384
+ return failed(read.failure);
385
+ const all = read.ok ? read.value.account_authenticators : [];
386
+ const authenticators = await Promise.all(
387
+ all.slice(0, AUTHENTICATOR_CAP).map(async (entry) => {
388
+ const type = TYPE_NAME.test(entry.type) ? entry.type : 'unknown';
389
+ return {
390
+ id: clip(entry.id, 20),
391
+ type,
392
+ kindLabel:
393
+ TYPE_LABEL[type] ??
394
+ 'an authenticator type this reader does not know',
395
+ rule: clip(
396
+ await ruleOf(
397
+ entry.type,
398
+ entry.config,
399
+ { nodes: RULE_NODES },
400
+ 1,
401
+ ),
402
+ RULE_CHARS,
403
+ ),
404
+ };
405
+ }),
406
+ );
407
+ return {
408
+ ok: true,
409
+ subject: account.subject,
410
+ data: {
411
+ address: account.address,
412
+ smartAccount: all.length > 0,
413
+ authenticators,
414
+ ...(all.length > AUTHENTICATOR_CAP
415
+ ? { moreAuthenticators: all.length - AUTHENTICATOR_CAP }
416
+ : {}),
417
+ },
418
+ };
419
+ },
420
+ };
421
+ },
422
+ });
423
+
424
+ /* ── spendable ────────────────────────────────────────────────────────── */
425
+
426
+ /** Bank denoms read per account (one page of each list). */
427
+ const BALANCE_PAGE = 50;
428
+
429
+ const balancesResponse = z.object({
430
+ balances: coinsSchema,
431
+ pagination: z.object({ next_key: z.string().nullish() }).nullish(),
432
+ });
433
+
434
+ export const spendableKind = defineKind({
435
+ name: 'spendable',
436
+ summary:
437
+ 'How much of each token an account can spend now, and how much is locked.',
438
+ explain: [
439
+ 'Per token, each as a coin: `total` = the bank balance, `spendable` = what the account can send or stake right now, `locked` = total − spendable: tokens still vesting (a vesting account) or otherwise held back by the chain. Staked tokens are not in the bank balance at all (see `staking`).',
440
+ 'Coins: `amount` in display units beside `symbol` (e.g. IXO) with `amountBaseUnits`; an unverified token (`verified: false`) is in base units. `anyLocked: false` = everything is spendable. `moreDenoms` = the account holds more than 50 denoms and the rest were not read.',
441
+ ].join('\n'),
442
+ source: 'chain',
443
+ list: false,
444
+ args,
445
+ example: {},
446
+ plan(input, ctx) {
447
+ const account = accountFor(ctx, input.address);
448
+ if ('invalid' in account) return account;
449
+ return {
450
+ source: 'chain',
451
+ async run(): Promise<KindResult> {
452
+ const query = `${account.address}?pagination.limit=${BALANCE_PAGE}`;
453
+ const [spendable, total] = await Promise.all([
454
+ ctx.rest(
455
+ `/cosmos/bank/v1beta1/spendable_balances/${query}`,
456
+ balancesResponse,
457
+ { label: 'spendable balances' },
458
+ ),
459
+ ctx.rest(`/cosmos/bank/v1beta1/balances/${query}`, balancesResponse, {
460
+ label: 'balances',
461
+ }),
462
+ ]);
463
+ if (!total.ok) return failed(total.failure);
464
+ if (!spendable.ok) return failed(spendable.failure);
465
+ const free = new Map(
466
+ spendable.value.balances.map((coin) => [
467
+ coin.denom,
468
+ wholeUnits(coin.amount) ?? '0',
469
+ ]),
470
+ );
471
+ const rows = total.value.balances.flatMap((coin) => {
472
+ const units = wholeUnits(coin.amount);
473
+ if (units === null) return [];
474
+ const now = BigInt(free.get(coin.denom) ?? '0');
475
+ const locked = BigInt(units) - now;
476
+ return [
477
+ {
478
+ denom: coin.denom,
479
+ total: units,
480
+ spendable: now.toString(),
481
+ locked: (locked > 0n ? locked : 0n).toString(),
482
+ },
483
+ ];
484
+ });
485
+ rows.sort(
486
+ (a, b) => Number(b.denom === 'uixo') - Number(a.denom === 'uixo'),
487
+ );
488
+ const displays = await displaysOf(
489
+ ctx,
490
+ rows.map((row) => row.denom),
491
+ );
492
+ const tokens = rows.flatMap((row) => {
493
+ const display = displays.get(row.denom);
494
+ if (!display) return [];
495
+ return [
496
+ {
497
+ total: coinView(row.denom, row.total, display),
498
+ spendable: coinView(row.denom, row.spendable, display),
499
+ locked: coinView(row.denom, row.locked, display),
500
+ },
501
+ ];
502
+ });
503
+ return {
504
+ ok: true,
505
+ subject: account.subject,
506
+ data: {
507
+ address: account.address,
508
+ anyLocked: rows.some((row) => row.locked !== '0'),
509
+ tokens,
510
+ ...(total.value.pagination?.next_key ? { moreDenoms: true } : {}),
511
+ },
512
+ };
513
+ },
514
+ };
515
+ },
516
+ });