@ixo/oracle-runtime-workers 0.27.0 → 0.28.2
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/README.md +1 -0
- package/package.json +3 -3
- package/src/core/domain-context/turn-context.ts +1 -1
- package/src/core/env.ts +5 -4
- package/src/core/main-agent.ts +31 -5
- package/src/core/prompt-composer.ts +14 -0
- package/src/core/sub-agent-fallback.ts +3 -2
- package/src/core/user-identity.ts +67 -0
- package/src/plugins/hub-actions/hub-actions.plugin.ts +6 -2
- package/src/plugins/index.ts +6 -2
- package/src/plugins/ixo-chain/blocksync.ts +214 -0
- package/src/plugins/ixo-chain/denoms.ts +493 -0
- package/src/plugins/ixo-chain/http.ts +388 -0
- package/src/plugins/ixo-chain/index.ts +1 -0
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +578 -0
- package/src/plugins/ixo-chain/queries.ts +401 -0
- package/src/plugins/ixo-chain/rest.ts +79 -0
- package/src/plugins/ixo-chain/summaries.ts +799 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a bank denom is shown. DenomMetadata is trusted for any denom: on
|
|
3
|
+
* the IXO chain it is written only by chain code (liquidstake pools, which
|
|
4
|
+
* governance creates; the IBC transfer module; genesis and upgrades), never
|
|
5
|
+
* by a token's issuer (the chain has no tokenfactory module). This is
|
|
6
|
+
* broader than the Auth Hub's rule (ixo-auth-worker
|
|
7
|
+
* `src/lib/actions/denom-display.ts`), which reads metadata only for
|
|
8
|
+
* `ibc/…`, so the Hub may still show a native token such as `upay` as
|
|
9
|
+
* unverified.
|
|
10
|
+
*
|
|
11
|
+
* - `uixo`: always IXO with 6 decimals, verified, no lookup.
|
|
12
|
+
* - an `ibc/…` denom in the IBC table (the built-in entries for the
|
|
13
|
+
* oracle's network, with `IXO_CHAIN_IBC_DENOMS` merged over them):
|
|
14
|
+
* verified, the entry's symbol, decimals and `origin`, no lookup.
|
|
15
|
+
* - any other denom with metadata whose `display` unit is listed in
|
|
16
|
+
* `denom_units` with an exponent of 0–18: verified, those decimals, the
|
|
17
|
+
* metadata `symbol` (or, when that is empty, the display unit
|
|
18
|
+
* upper-cased: `uatom` → `ATOM`) if it is plain ASCII.
|
|
19
|
+
* - an `ibc/…` denom whose metadata does not verify it (the shape IBC
|
|
20
|
+
* transfers register: a display unit not listed) but whose denom trace
|
|
21
|
+
* reads: unverified, base units, the full denom as its symbol, with a
|
|
22
|
+
* `trace`: the base denom, the chain id the IXO side's first channel's
|
|
23
|
+
* light client claims (null when that could not be read) and the path.
|
|
24
|
+
* Anyone can open a client claiming any chain id and send any base
|
|
25
|
+
* denom over it, so a trace describes, it never identifies.
|
|
26
|
+
* - anything else (no metadata, no trace, or a malformed denom, which is
|
|
27
|
+
* never looked up): unverified, base units (0 decimals), the full denom
|
|
28
|
+
* as its symbol.
|
|
29
|
+
*
|
|
30
|
+
* A denom whose metadata could not be read (a transport failure, or no
|
|
31
|
+
* budget left to ask) is shown the same way with `metadataUnavailable`:
|
|
32
|
+
* it may still be verified. Its trace is not read then.
|
|
33
|
+
*/
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
import { MIN_ATTEMPT_MS, throwIfAborted, type ChainCall } from './http';
|
|
36
|
+
import { restJson } from './rest';
|
|
37
|
+
|
|
38
|
+
export type DenomMetadataView = { symbol: string; decimals: number } | null;
|
|
39
|
+
export type TokenDisplay = {
|
|
40
|
+
symbol: string;
|
|
41
|
+
decimals: number;
|
|
42
|
+
verified: boolean;
|
|
43
|
+
/** The chain id a verified IBC token came from (from the IBC table only). */
|
|
44
|
+
origin?: string;
|
|
45
|
+
/** What an unverified IBC token's trace claims: self-reported by the sending chain, never proof. */
|
|
46
|
+
trace?: IbcTrace;
|
|
47
|
+
metadataUnavailable?: true;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** An unverified IBC token's denom trace, as the sending side reported it. */
|
|
51
|
+
export type IbcTrace = {
|
|
52
|
+
baseDenom: string;
|
|
53
|
+
/** The chain id the IXO side's first channel's light client claims; null when unread. */
|
|
54
|
+
claimedChain: string | null;
|
|
55
|
+
path: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** An IBC voucher denom: `ibc/` and the upper-case hex SHA-256 of its trace. */
|
|
59
|
+
const IBC_DENOM = /^ibc\/[A-F0-9]{64}$/;
|
|
60
|
+
/** A Cosmos chain id (`noble-1`, `injective-1`). */
|
|
61
|
+
const CHAIN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,49}$/;
|
|
62
|
+
|
|
63
|
+
/** A verified IBC token: its symbol, decimals and, when known, the chain id it came from. */
|
|
64
|
+
export type IbcDenomEntry = {
|
|
65
|
+
symbol: string;
|
|
66
|
+
decimals: number;
|
|
67
|
+
origin?: string;
|
|
68
|
+
};
|
|
69
|
+
export type IbcDenomTable = ReadonlyMap<string, IbcDenomEntry>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* IBC tokens shown verified without a lookup, per network. The IXO
|
|
73
|
+
* chain's own metadata for them lists only the base unit, so it cannot
|
|
74
|
+
* verify them. Each entry was checked on mainnet on Oct 29 2026:
|
|
75
|
+
*
|
|
76
|
+
* - `ibc/6BBE…F97B`: denom trace `transfer/channel-26` / `uusdc`;
|
|
77
|
+
* channel-26's client is for chain `noble-1`; Noble's `uusdc` metadata
|
|
78
|
+
* lists a `usdc` unit with exponent 6.
|
|
79
|
+
* - `ibc/77C1…55E6`: denom trace `transfer/channel-31` /
|
|
80
|
+
* `erc20:0xa00C59fF5a080D2b954d0c75e46E22a0c371235a`; channel-31's client
|
|
81
|
+
* is for chain `injective-1`; on Injective that token is USDC with 6
|
|
82
|
+
* decimals (the Auth Hub's USDC sweep lands it).
|
|
83
|
+
*
|
|
84
|
+
* Testnet has only `uosmo` and `uaxl` traces and devnet none; neither has
|
|
85
|
+
* an entry.
|
|
86
|
+
*/
|
|
87
|
+
export const KNOWN_IBC_DENOMS: Record<
|
|
88
|
+
ChainCall['network'],
|
|
89
|
+
Readonly<Record<string, IbcDenomEntry>>
|
|
90
|
+
> = {
|
|
91
|
+
mainnet: {
|
|
92
|
+
'ibc/6BBE9BD4246F8E04948D5A4EEE7164B2630263B9EBB5E7DC5F0A46C62A2FF97B': {
|
|
93
|
+
symbol: 'USDC',
|
|
94
|
+
decimals: 6,
|
|
95
|
+
origin: 'noble-1',
|
|
96
|
+
},
|
|
97
|
+
'ibc/77C12480BD8C29BF0ACEC2143736C15BAF8CD232CA27BAE09336677B828955E6': {
|
|
98
|
+
symbol: 'USDC',
|
|
99
|
+
decimals: 6,
|
|
100
|
+
origin: 'injective-1',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
testnet: {},
|
|
104
|
+
devnet: {},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** The shape of `IXO_CHAIN_IBC_DENOMS` once parsed: `{ "ibc/<HASH>": { symbol, decimals, origin? } }`. */
|
|
108
|
+
export const ibcDenomsSchema = z
|
|
109
|
+
.record(
|
|
110
|
+
z.string(),
|
|
111
|
+
z.strictObject({
|
|
112
|
+
symbol: z
|
|
113
|
+
.string()
|
|
114
|
+
.regex(
|
|
115
|
+
/^[A-Za-z0-9.-]{1,20}$/,
|
|
116
|
+
'1–20 characters of A–Z, a–z, 0–9, . or -',
|
|
117
|
+
),
|
|
118
|
+
decimals: z.number().int().min(0).max(18),
|
|
119
|
+
origin: z
|
|
120
|
+
.string()
|
|
121
|
+
.regex(CHAIN_ID, 'a chain id such as noble-1')
|
|
122
|
+
.optional(),
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
// Checked here rather than as the record's key schema, so the error
|
|
126
|
+
// names the key and the rule instead of "Invalid key in record".
|
|
127
|
+
.superRefine((entries, ctx) => {
|
|
128
|
+
for (const key of Object.keys(entries))
|
|
129
|
+
if (!IBC_DENOM.test(key))
|
|
130
|
+
ctx.addIssue({
|
|
131
|
+
code: 'custom',
|
|
132
|
+
path: [key],
|
|
133
|
+
message: 'keys must be ibc/ and 64 upper-case hex digits',
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `IXO_CHAIN_IBC_DENOMS` parsed: a JSON string (a secret or a string
|
|
139
|
+
* `vars` entry) or the object itself (an object `vars` entry); not ok,
|
|
140
|
+
* with the reason, when it is not valid JSON or not the expected shape.
|
|
141
|
+
*/
|
|
142
|
+
export function parseIbcDenoms(
|
|
143
|
+
value: unknown,
|
|
144
|
+
):
|
|
145
|
+
| { ok: true; entries: Record<string, IbcDenomEntry> }
|
|
146
|
+
| { ok: false; error: string } {
|
|
147
|
+
let raw = value;
|
|
148
|
+
if (typeof value === 'string')
|
|
149
|
+
try {
|
|
150
|
+
raw = JSON.parse(value);
|
|
151
|
+
} catch {
|
|
152
|
+
return { ok: false, error: 'must be a JSON object' };
|
|
153
|
+
}
|
|
154
|
+
const parsed = ibcDenomsSchema.safeParse(raw);
|
|
155
|
+
if (parsed.success) return { ok: true, entries: parsed.data };
|
|
156
|
+
const issue = parsed.error.issues[0];
|
|
157
|
+
const where = issue?.path.length ? `${issue.path.join('.')}: ` : '';
|
|
158
|
+
return { ok: false, error: `${where}${issue?.message ?? 'invalid'}` };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The IBC table of a call: the network's built-in entries with the
|
|
163
|
+
* operator's `IXO_CHAIN_IBC_DENOMS` merged over them. Boot validation
|
|
164
|
+
* rejects an invalid override, so one that does not parse here (a config
|
|
165
|
+
* that skipped it) leaves the built-in entries alone.
|
|
166
|
+
*/
|
|
167
|
+
export function ibcDenomTable(
|
|
168
|
+
network: ChainCall['network'],
|
|
169
|
+
override: unknown,
|
|
170
|
+
): IbcDenomTable {
|
|
171
|
+
const extra = override === undefined ? null : parseIbcDenoms(override);
|
|
172
|
+
return new Map(
|
|
173
|
+
Object.entries({
|
|
174
|
+
...KNOWN_IBC_DENOMS[network],
|
|
175
|
+
...(extra?.ok ? extra.entries : {}),
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Symbols an unverified denom may never use (compared case-insensitively). */
|
|
181
|
+
const RESERVED_SYMBOLS = ['IXO', 'USDC', 'USDT', 'ATOM', 'OSMO'] as const;
|
|
182
|
+
/** The Cosmos SDK bank denom pattern: only such a denom is looked up. */
|
|
183
|
+
const DENOM = /^[a-zA-Z][a-zA-Z0-9/:._-]{2,127}$/;
|
|
184
|
+
// Plain ASCII only: no spaces or look-alike Unicode letters ("UЅDC").
|
|
185
|
+
const SAFE_SYMBOL = /^[A-Za-z0-9._-]{1,32}$/;
|
|
186
|
+
|
|
187
|
+
export function trustedDenomDisplay(
|
|
188
|
+
denom: string,
|
|
189
|
+
metadata: DenomMetadataView,
|
|
190
|
+
): TokenDisplay {
|
|
191
|
+
if (denom === 'uixo') return { symbol: 'IXO', decimals: 6, verified: true };
|
|
192
|
+
const symbol =
|
|
193
|
+
metadata && SAFE_SYMBOL.test(metadata.symbol) ? metadata.symbol : null;
|
|
194
|
+
if (
|
|
195
|
+
metadata &&
|
|
196
|
+
symbol &&
|
|
197
|
+
DENOM.test(denom) &&
|
|
198
|
+
Number.isInteger(metadata.decimals) &&
|
|
199
|
+
metadata.decimals >= 0 &&
|
|
200
|
+
metadata.decimals <= 18
|
|
201
|
+
)
|
|
202
|
+
return { symbol, decimals: metadata.decimals, verified: true };
|
|
203
|
+
const reserved =
|
|
204
|
+
symbol !== null &&
|
|
205
|
+
(RESERVED_SYMBOLS as readonly string[]).includes(symbol.toUpperCase());
|
|
206
|
+
return {
|
|
207
|
+
symbol: symbol && !reserved ? symbol : denom,
|
|
208
|
+
decimals: 0,
|
|
209
|
+
verified: false,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Base units as a decimal string: `("9000000", 6)` → `"9"`, `("1234567", 6)` → `"1.234567"`. */
|
|
214
|
+
export function formatUnits(amount: string, decimals: number): string {
|
|
215
|
+
if (decimals === 0) return amount;
|
|
216
|
+
const padded = amount.padStart(decimals + 1, '0');
|
|
217
|
+
const whole = padded.slice(0, -decimals);
|
|
218
|
+
const fraction = padded.slice(-decimals).replace(/0+$/, '');
|
|
219
|
+
return fraction ? `${whole}.${fraction}` : whole;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Per isolate: found metadata is reused for 10 min, a definite miss (404,
|
|
224
|
+
* or no display unit) for 60 s, a failed read for 10 s — a failure is
|
|
225
|
+
* never kept as a miss, so it cannot leave a real token unverified.
|
|
226
|
+
*/
|
|
227
|
+
export const METADATA_TTL_MS = 600_000;
|
|
228
|
+
export const METADATA_NOT_FOUND_TTL_MS = 60_000;
|
|
229
|
+
export const METADATA_FAILED_TTL_MS = 10_000;
|
|
230
|
+
/** A denom trace or a channel's chain id never changes: found ones are reused for 1 h (misses and failures as for metadata). */
|
|
231
|
+
export const IBC_TRACE_TTL_MS = 3_600_000;
|
|
232
|
+
const CACHE_LIMIT = 256;
|
|
233
|
+
|
|
234
|
+
/** One read's answer: the value (null for a miss), or why there is none. */
|
|
235
|
+
type Lookup<T> = { value: T | null } | 'failed' | 'aborted';
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Answers per `${restUrl}|${key}`, kept per isolate: a found value for
|
|
239
|
+
* `foundTtlMs`, a miss for 60 s, a failure for 10 s, an abort never. A
|
|
240
|
+
* read in flight is shared by every call of the isolate that needs the
|
|
241
|
+
* same key; none is started with less than 1 s of the budget left.
|
|
242
|
+
*/
|
|
243
|
+
class ReadCache<T> {
|
|
244
|
+
private readonly answers = new Map<
|
|
245
|
+
string,
|
|
246
|
+
{ lookup: Lookup<T>; expiresAt: number }
|
|
247
|
+
>();
|
|
248
|
+
private readonly inflight = new Map<string, Promise<Lookup<T>>>();
|
|
249
|
+
|
|
250
|
+
constructor(private readonly foundTtlMs: number) {}
|
|
251
|
+
|
|
252
|
+
get(
|
|
253
|
+
call: ChainCall,
|
|
254
|
+
key: string,
|
|
255
|
+
read: () => Promise<Lookup<T>>,
|
|
256
|
+
): Promise<Lookup<T>> {
|
|
257
|
+
const id = `${call.restUrl}|${key}`;
|
|
258
|
+
const hit = this.answers.get(id);
|
|
259
|
+
if (hit && hit.expiresAt > Date.now()) return Promise.resolve(hit.lookup);
|
|
260
|
+
const running = this.inflight.get(id);
|
|
261
|
+
if (running) return running;
|
|
262
|
+
if (call.deadline - Date.now() < MIN_ATTEMPT_MS)
|
|
263
|
+
return Promise.resolve('failed');
|
|
264
|
+
const pending = read()
|
|
265
|
+
.then((lookup) => {
|
|
266
|
+
this.remember(id, lookup);
|
|
267
|
+
return lookup;
|
|
268
|
+
})
|
|
269
|
+
.finally(() => this.inflight.delete(id));
|
|
270
|
+
this.inflight.set(id, pending);
|
|
271
|
+
return pending;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
clear(): void {
|
|
275
|
+
this.answers.clear();
|
|
276
|
+
this.inflight.clear();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private remember(id: string, lookup: Lookup<T>): void {
|
|
280
|
+
if (lookup === 'aborted') return;
|
|
281
|
+
const ttl =
|
|
282
|
+
lookup === 'failed'
|
|
283
|
+
? METADATA_FAILED_TTL_MS
|
|
284
|
+
: lookup.value
|
|
285
|
+
? this.foundTtlMs
|
|
286
|
+
: METADATA_NOT_FOUND_TTL_MS;
|
|
287
|
+
if (this.answers.size > CACHE_LIMIT) this.answers.clear();
|
|
288
|
+
this.answers.set(id, { lookup, expiresAt: Date.now() + ttl });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
type DenomTrace = { baseDenom: string; path: string; channel: string };
|
|
293
|
+
|
|
294
|
+
const metadataCache = new ReadCache<{ symbol: string; decimals: number }>(
|
|
295
|
+
METADATA_TTL_MS,
|
|
296
|
+
);
|
|
297
|
+
const traceCache = new ReadCache<DenomTrace>(IBC_TRACE_TTL_MS);
|
|
298
|
+
const chainIdCache = new ReadCache<string>(IBC_TRACE_TTL_MS);
|
|
299
|
+
|
|
300
|
+
/** Tests only: forget every cached metadata, trace and channel answer. */
|
|
301
|
+
export function resetDenomMetadataCache(): void {
|
|
302
|
+
metadataCache.clear();
|
|
303
|
+
traceCache.clear();
|
|
304
|
+
chainIdCache.clear();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* One bounded REST read of a resource that may be absent: a 404 is a
|
|
309
|
+
* miss, any other failure `failed`, the launching turn's abort `aborted`
|
|
310
|
+
* (so a call sharing the read is not failed by another turn's abort).
|
|
311
|
+
*/
|
|
312
|
+
async function readRest<S extends z.ZodType, T>(
|
|
313
|
+
call: ChainCall,
|
|
314
|
+
path: string,
|
|
315
|
+
schema: S,
|
|
316
|
+
label: string,
|
|
317
|
+
pick: (value: z.infer<S>) => T | null,
|
|
318
|
+
): Promise<Lookup<T>> {
|
|
319
|
+
let read;
|
|
320
|
+
try {
|
|
321
|
+
read = await restJson(call, path, schema, {
|
|
322
|
+
label,
|
|
323
|
+
maxBytes: 64 * 1024,
|
|
324
|
+
notFoundOn404: true,
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (call.signal.aborted) return 'aborted';
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
if (!read.ok)
|
|
331
|
+
return read.failure.reason === 'not_found' ? { value: null } : 'failed';
|
|
332
|
+
return { value: pick(read.value) };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const metadataResponse = z.object({
|
|
336
|
+
metadata: z.object({
|
|
337
|
+
denom_units: z.array(
|
|
338
|
+
z.object({ denom: z.string(), exponent: z.number().int() }),
|
|
339
|
+
),
|
|
340
|
+
display: z.string(),
|
|
341
|
+
symbol: z.string(),
|
|
342
|
+
}),
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* One DenomMetadata read. The by-query-string route is used because the
|
|
347
|
+
* path route cannot carry the slash in `ibc/…` (the gateway answers 501).
|
|
348
|
+
* Metadata whose display unit is not listed with an exponent of 0–18 is a
|
|
349
|
+
* miss.
|
|
350
|
+
*/
|
|
351
|
+
function readMetadata(
|
|
352
|
+
call: ChainCall,
|
|
353
|
+
denom: string,
|
|
354
|
+
): Promise<Lookup<{ symbol: string; decimals: number }>> {
|
|
355
|
+
return readRest(
|
|
356
|
+
call,
|
|
357
|
+
`/cosmos/bank/v1beta1/denoms_metadata_by_query_string?denom=${encodeURIComponent(denom)}`,
|
|
358
|
+
metadataResponse,
|
|
359
|
+
'denom metadata',
|
|
360
|
+
({ metadata }) => {
|
|
361
|
+
const unit = metadata.denom_units.find(
|
|
362
|
+
(u) => u.denom === metadata.display,
|
|
363
|
+
);
|
|
364
|
+
return unit && unit.exponent >= 0 && unit.exponent <= 18
|
|
365
|
+
? {
|
|
366
|
+
symbol: metadata.symbol || metadata.display.toUpperCase(),
|
|
367
|
+
decimals: unit.exponent,
|
|
368
|
+
}
|
|
369
|
+
: null;
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const traceResponse = z.object({
|
|
375
|
+
denom_trace: z.object({ path: z.string(), base_denom: z.string() }),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* An ICS-20 trace path as the IXO chain records it: hops of
|
|
380
|
+
* `port/channel-N`, the first one the IXO side's own `transfer` channel.
|
|
381
|
+
*/
|
|
382
|
+
const TRACE_PATH =
|
|
383
|
+
/^transfer\/(channel-\d{1,6})(?:\/[A-Za-z0-9._-]{1,64}\/channel-\d{1,6}){0,15}$/;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* One denom trace read (`/ibc/apps/transfer/v1/denom_traces/{hash}`; the
|
|
387
|
+
* newer `denoms/{hash}` route answers 501 on the IXO chain). A trace
|
|
388
|
+
* whose path or base denom does not have the expected shape is a miss.
|
|
389
|
+
*/
|
|
390
|
+
function readTrace(call: ChainCall, hash: string): Promise<Lookup<DenomTrace>> {
|
|
391
|
+
return readRest(
|
|
392
|
+
call,
|
|
393
|
+
`/ibc/apps/transfer/v1/denom_traces/${hash}`,
|
|
394
|
+
traceResponse,
|
|
395
|
+
'denom trace',
|
|
396
|
+
({ denom_trace: { path, base_denom: baseDenom } }) => {
|
|
397
|
+
const channel = TRACE_PATH.exec(path)?.[1];
|
|
398
|
+
return channel && DENOM.test(baseDenom)
|
|
399
|
+
? { baseDenom, path, channel }
|
|
400
|
+
: null;
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Light clients without a chain id (not Tendermint) answer without `chain_id`. */
|
|
406
|
+
const clientStateResponse = z.object({
|
|
407
|
+
identified_client_state: z.object({
|
|
408
|
+
client_state: z.object({ chain_id: z.string().optional() }),
|
|
409
|
+
}),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
/** The chain id of the client behind the IXO chain's `transfer` port on `channel`. */
|
|
413
|
+
function readChainId(
|
|
414
|
+
call: ChainCall,
|
|
415
|
+
channel: string,
|
|
416
|
+
): Promise<Lookup<string>> {
|
|
417
|
+
return readRest(
|
|
418
|
+
call,
|
|
419
|
+
`/ibc/core/channel/v1/channels/${channel}/ports/transfer/client_state`,
|
|
420
|
+
clientStateResponse,
|
|
421
|
+
'channel client state',
|
|
422
|
+
({ identified_client_state: { client_state } }) =>
|
|
423
|
+
client_state.chain_id && CHAIN_ID.test(client_state.chain_id)
|
|
424
|
+
? client_state.chain_id
|
|
425
|
+
: null,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* `fallback` with the denom trace of `denom` (an `ibc/…` denom the
|
|
431
|
+
* metadata did not verify) beside it: the base denom, the path and the
|
|
432
|
+
* chain id its first channel's client claims (null when that read
|
|
433
|
+
* failed). The symbol stays the full denom: a trace is self-reported.
|
|
434
|
+
* `fallback` itself when the trace cannot be read.
|
|
435
|
+
*/
|
|
436
|
+
async function tracedDisplay(
|
|
437
|
+
call: ChainCall,
|
|
438
|
+
denom: string,
|
|
439
|
+
fallback: TokenDisplay,
|
|
440
|
+
): Promise<TokenDisplay> {
|
|
441
|
+
const hash = denom.slice('ibc/'.length);
|
|
442
|
+
const trace = await traceCache.get(call, hash, () => readTrace(call, hash));
|
|
443
|
+
throwIfAborted(call.signal);
|
|
444
|
+
if (trace === 'failed' || trace === 'aborted' || !trace.value)
|
|
445
|
+
return fallback;
|
|
446
|
+
const { baseDenom, path, channel } = trace.value;
|
|
447
|
+
const chainId = await chainIdCache.get(call, channel, () =>
|
|
448
|
+
readChainId(call, channel),
|
|
449
|
+
);
|
|
450
|
+
throwIfAborted(call.signal);
|
|
451
|
+
return {
|
|
452
|
+
...fallback,
|
|
453
|
+
trace: {
|
|
454
|
+
baseDenom,
|
|
455
|
+
claimedChain:
|
|
456
|
+
chainId === 'failed' || chainId === 'aborted' ? null : chainId.value,
|
|
457
|
+
path,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* The display of `denom`: `uixo` and an `ibc/…` denom in `ibcDenoms`
|
|
464
|
+
* without a lookup; any other denom through its DenomMetadata, then (an
|
|
465
|
+
* `ibc/…` denom it does not verify) its denom trace.
|
|
466
|
+
*/
|
|
467
|
+
export async function denomDisplay(
|
|
468
|
+
call: ChainCall,
|
|
469
|
+
denom: string,
|
|
470
|
+
ibcDenoms: IbcDenomTable,
|
|
471
|
+
): Promise<TokenDisplay> {
|
|
472
|
+
if (denom === 'uixo' || !DENOM.test(denom))
|
|
473
|
+
return trustedDenomDisplay(denom, null);
|
|
474
|
+
const ibc = IBC_DENOM.test(denom);
|
|
475
|
+
const known = ibc ? ibcDenoms.get(denom) : undefined;
|
|
476
|
+
if (known)
|
|
477
|
+
return {
|
|
478
|
+
symbol: known.symbol,
|
|
479
|
+
decimals: known.decimals,
|
|
480
|
+
verified: true,
|
|
481
|
+
...(known.origin ? { origin: known.origin } : {}),
|
|
482
|
+
};
|
|
483
|
+
const lookup = await metadataCache.get(call, denom, () =>
|
|
484
|
+
readMetadata(call, denom),
|
|
485
|
+
);
|
|
486
|
+
throwIfAborted(call.signal);
|
|
487
|
+
if (lookup === 'failed' || lookup === 'aborted')
|
|
488
|
+
return { ...trustedDenomDisplay(denom, null), metadataUnavailable: true };
|
|
489
|
+
const display = trustedDenomDisplay(denom, lookup.value);
|
|
490
|
+
return display.verified || !ibc
|
|
491
|
+
? display
|
|
492
|
+
: tracedDisplay(call, denom, display);
|
|
493
|
+
}
|