@aigne/afs-ocap 1.12.0-beta.5
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/LICENSE.md +26 -0
- package/README.md +144 -0
- package/aup/accounts/default.json +64 -0
- package/aup/accounts/item.json +510 -0
- package/aup/assets/default.json +70 -0
- package/aup/assets/item.json +360 -0
- package/aup/bridges/_addr/blocks/item.json +168 -0
- package/aup/bridges/default.json +79 -0
- package/aup/bridges/item.json +868 -0
- package/aup/config/default.json +806 -0
- package/aup/default.json +387 -0
- package/aup/delegates/default.json +71 -0
- package/aup/delegates/item.json +241 -0
- package/aup/factories/default.json +78 -0
- package/aup/factories/item.json +310 -0
- package/aup/stakes/default.json +76 -0
- package/aup/stakes/item.json +374 -0
- package/aup/tokens/default.json +76 -0
- package/aup/tokens/item.json +384 -0
- package/aup/transactions/default.json +102 -0
- package/aup/transactions/item.json +286 -0
- package/aup/validators/default.json +57 -0
- package/aup/validators/item.json +99 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/index.cjs +7 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +4 -0
- package/dist/method-colors.cjs +137 -0
- package/dist/method-colors.mjs +136 -0
- package/dist/method-colors.mjs.map +1 -0
- package/dist/provider.cjs +2490 -0
- package/dist/provider.d.cts +453 -0
- package/dist/provider.d.cts.map +1 -0
- package/dist/provider.d.mts +453 -0
- package/dist/provider.d.mts.map +1 -0
- package/dist/provider.mjs +2490 -0
- package/dist/provider.mjs.map +1 -0
- package/dist/remote-source.cjs +338 -0
- package/dist/remote-source.d.cts +62 -0
- package/dist/remote-source.d.cts.map +1 -0
- package/dist/remote-source.d.mts +62 -0
- package/dist/remote-source.d.mts.map +1 -0
- package/dist/remote-source.mjs +339 -0
- package/dist/remote-source.mjs.map +1 -0
- package/dist/source.d.cts +77 -0
- package/dist/source.d.cts.map +1 -0
- package/dist/source.d.mts +77 -0
- package/dist/source.d.mts.map +1 -0
- package/dist/types.d.cts +572 -0
- package/dist/types.d.cts.map +1 -0
- package/dist/types.d.mts +572 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/utils.cjs +581 -0
- package/dist/utils.mjs +557 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +61 -0
package/dist/utils.cjs
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
let _aigne_afs = require("@aigne/afs");
|
|
2
|
+
let ufo = require("ufo");
|
|
3
|
+
let _arcblock_did = require("@arcblock/did");
|
|
4
|
+
let _arcblock_did_util_cbor = require("@arcblock/did-util/cbor");
|
|
5
|
+
|
|
6
|
+
//#region src/utils.ts
|
|
7
|
+
const TOP_LEVEL_DIRS = [
|
|
8
|
+
"config",
|
|
9
|
+
"validators",
|
|
10
|
+
"transactions",
|
|
11
|
+
"accounts",
|
|
12
|
+
"tokens",
|
|
13
|
+
"assets",
|
|
14
|
+
"factories",
|
|
15
|
+
"stakes",
|
|
16
|
+
"bridges",
|
|
17
|
+
"delegates"
|
|
18
|
+
];
|
|
19
|
+
/** Collection path segment → singular kind suffix. Single source of truth. */
|
|
20
|
+
const COLLECTION_KIND_MAP = {
|
|
21
|
+
config: "config",
|
|
22
|
+
validators: "validator",
|
|
23
|
+
transactions: "transaction",
|
|
24
|
+
accounts: "account",
|
|
25
|
+
tokens: "token",
|
|
26
|
+
assets: "asset",
|
|
27
|
+
factories: "factory",
|
|
28
|
+
stakes: "stake",
|
|
29
|
+
bridges: "bridge",
|
|
30
|
+
delegates: "delegate"
|
|
31
|
+
};
|
|
32
|
+
/** Search result type → collection path segment. */
|
|
33
|
+
const TYPE_TO_COLLECTION = {
|
|
34
|
+
tx: "transactions",
|
|
35
|
+
transaction: "transactions",
|
|
36
|
+
account: "accounts",
|
|
37
|
+
token: "tokens",
|
|
38
|
+
asset: "assets",
|
|
39
|
+
factory: "factories",
|
|
40
|
+
stake: "stakes",
|
|
41
|
+
bridge: "bridges",
|
|
42
|
+
delegate: "delegates"
|
|
43
|
+
};
|
|
44
|
+
/** Returns the full `ocap:xxx` kind for a collection name. */
|
|
45
|
+
function collectionKind(collection) {
|
|
46
|
+
return `ocap:${COLLECTION_KIND_MAP[collection] ?? collection}`;
|
|
47
|
+
}
|
|
48
|
+
function toPaging(options, defaultSize = 20) {
|
|
49
|
+
return {
|
|
50
|
+
size: options?.limit ?? defaultSize,
|
|
51
|
+
offset: options?.offset ?? 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Single-point narrow from `ctx.options` (typed as `RouteOptions` union in
|
|
56
|
+
* `@aigne/afs/provider`) down to OCAP's domain-specific `OcapListOptions`
|
|
57
|
+
* shape. Use this in every `@List` handler instead of casting through
|
|
58
|
+
* `as any` / `as Record<string, unknown>` — the runtime cost is identical
|
|
59
|
+
* (a bare assertion, no copy), but the type system now knows what fields
|
|
60
|
+
* the handler may read, and `toPaging` + `extract*Filters` accept the
|
|
61
|
+
* narrowed type directly without further casts.
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* Typed-where vocabulary (afs-collection-query T5.3): every field is one of
|
|
65
|
+
* OCAP's documented filter keys, with the operator(s) its extractor shape
|
|
66
|
+
* admits. ANYTHING else — unknown field, disallowed operator, non-string
|
|
67
|
+
* value — is a strict `AFSValidationError`; no unvetted token can ever
|
|
68
|
+
* reach a GQL variable. (The core grammar validator has already rejected
|
|
69
|
+
* prototype keys / malformed dot-paths before this table is consulted.)
|
|
70
|
+
*/
|
|
71
|
+
const WHERE_SCALAR_FIELDS = new Set([
|
|
72
|
+
"sender",
|
|
73
|
+
"receiver",
|
|
74
|
+
"direction",
|
|
75
|
+
"validity",
|
|
76
|
+
"issuerAddress",
|
|
77
|
+
"ownerAddress",
|
|
78
|
+
"factoryAddress",
|
|
79
|
+
"tokenAddress",
|
|
80
|
+
"from",
|
|
81
|
+
"to"
|
|
82
|
+
]);
|
|
83
|
+
const WHERE_LIST_FIELDS = new Set([
|
|
84
|
+
"types",
|
|
85
|
+
"tokens",
|
|
86
|
+
"assets",
|
|
87
|
+
"factories",
|
|
88
|
+
"accounts",
|
|
89
|
+
"rollups",
|
|
90
|
+
"stakes",
|
|
91
|
+
"delegations",
|
|
92
|
+
"addressList"
|
|
93
|
+
]);
|
|
94
|
+
/** `time` leaves map gte→startDateTime / lte→endDateTime (ISO strings). */
|
|
95
|
+
const WHERE_TIME_FIELD = "time";
|
|
96
|
+
/** A leaf whose operator value is "" or [] is the form's untouched state. */
|
|
97
|
+
function isEmptySentinelLeaf(leaf) {
|
|
98
|
+
if (typeof leaf !== "object" || leaf === null) return false;
|
|
99
|
+
const rec = leaf;
|
|
100
|
+
for (const op of [
|
|
101
|
+
"eq",
|
|
102
|
+
"in",
|
|
103
|
+
"notIn",
|
|
104
|
+
"contains",
|
|
105
|
+
"gte",
|
|
106
|
+
"lte"
|
|
107
|
+
]) if (op in rec) {
|
|
108
|
+
const v = rec[op];
|
|
109
|
+
return v === "" || Array.isArray(v) && v.length === 0;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
/** Drop empty-sentinel leaves; undefined when nothing substantive remains. */
|
|
114
|
+
function pruneEmptySentinels(where) {
|
|
115
|
+
if (typeof where !== "object" || where === null) return where;
|
|
116
|
+
const rec = where;
|
|
117
|
+
if (Array.isArray(rec.all)) {
|
|
118
|
+
const kept = rec.all.filter((leaf) => !isEmptySentinelLeaf(leaf));
|
|
119
|
+
if (kept.length === 0) return void 0;
|
|
120
|
+
return { all: kept };
|
|
121
|
+
}
|
|
122
|
+
if (typeof rec.field === "string" && isEmptySentinelLeaf(rec)) return void 0;
|
|
123
|
+
return where;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Translate a typed `where` into the legacy private option keys the 7
|
|
127
|
+
* `extract*Filters` entry points consume — the GQL filter structure the
|
|
128
|
+
* extractors emit is therefore byte-identical to the private-key path
|
|
129
|
+
* (design §6: chain-explorer / OCAP row). Empty string / empty array values
|
|
130
|
+
* skip the leaf, mirroring the extractors' own empty-sentinel semantics.
|
|
131
|
+
*/
|
|
132
|
+
function whereToOcapOptions(where) {
|
|
133
|
+
const pruned = pruneEmptySentinels(where);
|
|
134
|
+
if (pruned === void 0) return {};
|
|
135
|
+
(0, _aigne_afs.validateCollectionQuerySpec)({ where: pruned });
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const leaf of (0, _aigne_afs.normalizeWhere)(pruned)) {
|
|
138
|
+
const field = leaf.field;
|
|
139
|
+
const op = Object.keys(leaf).filter((k) => k !== "field")[0];
|
|
140
|
+
const value = leaf[op];
|
|
141
|
+
if (field === WHERE_TIME_FIELD) {
|
|
142
|
+
if (op !== "gte" && op !== "lte") throw new _aigne_afs.AFSValidationError(`Invalid query spec: where.${field}: time accepts gte/lte only (got ${op})`);
|
|
143
|
+
if (typeof value !== "string") throw new _aigne_afs.AFSValidationError(`Invalid query spec: where.${field}: ISO string required`);
|
|
144
|
+
if (value === "") continue;
|
|
145
|
+
out[op === "gte" ? "startDateTime" : "endDateTime"] = value;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (WHERE_SCALAR_FIELDS.has(field)) {
|
|
149
|
+
if (op !== "eq") throw new _aigne_afs.AFSValidationError(`Invalid query spec: where.${field}: scalar filter accepts eq only (got ${op})`);
|
|
150
|
+
if (typeof value !== "string") throw new _aigne_afs.AFSValidationError(`Invalid query spec: where.${field}: string value required`);
|
|
151
|
+
if (value === "") continue;
|
|
152
|
+
out[field] = value;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (WHERE_LIST_FIELDS.has(field)) {
|
|
156
|
+
if (op !== "eq" && op !== "in") throw new _aigne_afs.AFSValidationError(`Invalid query spec: where.${field}: list filter accepts eq/in only (got ${op})`);
|
|
157
|
+
const arr = op === "in" ? value : [value];
|
|
158
|
+
if (!arr.every((v) => typeof v === "string")) throw new _aigne_afs.AFSValidationError(`Invalid query spec: where.${field}: string values required`);
|
|
159
|
+
const cleaned = arr.filter((v) => v.length > 0);
|
|
160
|
+
if (cleaned.length === 0) continue;
|
|
161
|
+
out[field] = cleaned;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
throw new _aigne_afs.AFSValidationError(`Invalid query spec: where.${field}: not an OCAP filter field`);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
function listOpts(ctx) {
|
|
169
|
+
const raw = ctx.options;
|
|
170
|
+
if (!raw || raw.where === void 0 || raw.where === null) return raw;
|
|
171
|
+
return {
|
|
172
|
+
...raw,
|
|
173
|
+
...whereToOcapOptions(raw.where)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function buildDirEntry(basePath, name, childrenCount) {
|
|
177
|
+
const isHidden = name === ".." || name.startsWith(".");
|
|
178
|
+
const meta = {
|
|
179
|
+
kind: collectionKind(name),
|
|
180
|
+
childrenCount: childrenCount ?? -1
|
|
181
|
+
};
|
|
182
|
+
if (isHidden) meta.visibility = "hidden";
|
|
183
|
+
return {
|
|
184
|
+
id: name,
|
|
185
|
+
path: (0, ufo.joinURL)(basePath, name),
|
|
186
|
+
meta,
|
|
187
|
+
summary: name
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function buildListResult(entries, total) {
|
|
191
|
+
return {
|
|
192
|
+
data: entries,
|
|
193
|
+
total
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** Reverse mapping: collection path segment → search result types. */
|
|
197
|
+
const COLLECTION_TO_TYPES = {};
|
|
198
|
+
for (const [type, col] of Object.entries(TYPE_TO_COLLECTION)) {
|
|
199
|
+
if (!COLLECTION_TO_TYPES[col]) COLLECTION_TO_TYPES[col] = [];
|
|
200
|
+
COLLECTION_TO_TYPES[col].push(type);
|
|
201
|
+
}
|
|
202
|
+
function pickDefined(obj) {
|
|
203
|
+
const result = {};
|
|
204
|
+
let hasAny = false;
|
|
205
|
+
for (const [k, v] of Object.entries(obj)) if (v !== void 0 && v !== null) {
|
|
206
|
+
result[k] = v;
|
|
207
|
+
hasAny = true;
|
|
208
|
+
}
|
|
209
|
+
return hasAny ? result : void 0;
|
|
210
|
+
}
|
|
211
|
+
function extractTimeFilter(opts) {
|
|
212
|
+
const start = opts?.startDateTime ?? opts?.timeStart;
|
|
213
|
+
const end = opts?.endDateTime ?? opts?.timeEnd;
|
|
214
|
+
if (!start && !end) return void 0;
|
|
215
|
+
const filter = {};
|
|
216
|
+
if (start) filter.startDateTime = start;
|
|
217
|
+
if (end) filter.endDateTime = end;
|
|
218
|
+
return filter;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Normalize the user-supplied validity value to the GQL enum. Accepts the
|
|
222
|
+
* canonical strings (`"VALID"` / `"INVALID"` / `"BOTH"`), the lowercase
|
|
223
|
+
* variants emitted by URL params, and the empty / undefined sentinels meaning
|
|
224
|
+
* "no filter". Any other value is dropped so an arbitrary URL param can't
|
|
225
|
+
* smuggle a non-enum string through to the GQL layer.
|
|
226
|
+
*/
|
|
227
|
+
function normalizeValidity(value) {
|
|
228
|
+
if (typeof value !== "string" || !value) return void 0;
|
|
229
|
+
const upper = value.toUpperCase();
|
|
230
|
+
if (upper === "VALID" || upper === "INVALID" || upper === "BOTH") return upper;
|
|
231
|
+
}
|
|
232
|
+
function extractTxFilters(opts) {
|
|
233
|
+
if (!opts) return void 0;
|
|
234
|
+
let addressFilter;
|
|
235
|
+
if (opts.sender || opts.receiver) {
|
|
236
|
+
addressFilter = { direction: opts.direction ?? "UNION" };
|
|
237
|
+
if (opts.sender) addressFilter.sender = opts.sender;
|
|
238
|
+
if (opts.receiver) addressFilter.receiver = opts.receiver;
|
|
239
|
+
}
|
|
240
|
+
const validity = normalizeValidity(opts.validity);
|
|
241
|
+
const toList = (v) => {
|
|
242
|
+
if (Array.isArray(v)) {
|
|
243
|
+
const cleaned = v.filter((x) => typeof x === "string" && x.length > 0);
|
|
244
|
+
return cleaned.length > 0 ? cleaned : void 0;
|
|
245
|
+
}
|
|
246
|
+
if (typeof v === "string" && v.length > 0) return [v];
|
|
247
|
+
};
|
|
248
|
+
const types = toList(opts.types);
|
|
249
|
+
const tokens = toList(opts.tokens);
|
|
250
|
+
return pickDefined({
|
|
251
|
+
addressFilter,
|
|
252
|
+
typeFilter: types ? { types } : void 0,
|
|
253
|
+
tokenFilter: tokens ? { tokens } : void 0,
|
|
254
|
+
assetFilter: opts.assets ? { assets: opts.assets } : void 0,
|
|
255
|
+
factoryFilter: opts.factories ? { factories: opts.factories } : void 0,
|
|
256
|
+
accountFilter: opts.accounts ? { accounts: opts.accounts } : void 0,
|
|
257
|
+
rollupFilter: opts.rollups ? { rollups: opts.rollups } : void 0,
|
|
258
|
+
stakeFilter: opts.stakes ? { stakes: opts.stakes } : void 0,
|
|
259
|
+
delegationFilter: opts.delegations ? { delegations: opts.delegations } : void 0,
|
|
260
|
+
validityFilter: validity && validity !== "BOTH" ? { validity } : void 0,
|
|
261
|
+
timeFilter: extractTimeFilter(opts)
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function extractTokenFilters(opts) {
|
|
265
|
+
if (!opts) return void 0;
|
|
266
|
+
return pickDefined({
|
|
267
|
+
issuerAddress: opts.issuerAddress,
|
|
268
|
+
timeFilter: extractTimeFilter(opts)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function extractAssetFilters(opts) {
|
|
272
|
+
if (!opts) return void 0;
|
|
273
|
+
return pickDefined({
|
|
274
|
+
ownerAddress: opts.ownerAddress,
|
|
275
|
+
factoryAddress: opts.factoryAddress,
|
|
276
|
+
timeFilter: extractTimeFilter(opts)
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function extractFactoryFilters(opts) {
|
|
280
|
+
if (!opts) return void 0;
|
|
281
|
+
return pickDefined({
|
|
282
|
+
ownerAddress: opts.ownerAddress,
|
|
283
|
+
addressList: opts.addressList,
|
|
284
|
+
timeFilter: extractTimeFilter(opts)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function extractStakeFilters(opts) {
|
|
288
|
+
if (!opts) return void 0;
|
|
289
|
+
let addressFilter;
|
|
290
|
+
if (opts.sender || opts.receiver) {
|
|
291
|
+
addressFilter = { direction: opts.direction ?? "UNION" };
|
|
292
|
+
if (opts.sender) addressFilter.sender = opts.sender;
|
|
293
|
+
if (opts.receiver) addressFilter.receiver = opts.receiver;
|
|
294
|
+
}
|
|
295
|
+
return pickDefined({
|
|
296
|
+
addressFilter,
|
|
297
|
+
timeFilter: extractTimeFilter(opts)
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function extractBridgeFilters(opts) {
|
|
301
|
+
if (!opts) return void 0;
|
|
302
|
+
return pickDefined({
|
|
303
|
+
tokenAddress: opts.tokenAddress,
|
|
304
|
+
timeFilter: extractTimeFilter(opts)
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function extractDelegationFilters(opts) {
|
|
308
|
+
if (!opts) return void 0;
|
|
309
|
+
return pickDefined({
|
|
310
|
+
from: opts.from,
|
|
311
|
+
to: opts.to,
|
|
312
|
+
timeFilter: extractTimeFilter(opts)
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/** DID role code → OCAP entity type name. */
|
|
316
|
+
const ROLE_TO_ENTITY = {
|
|
317
|
+
[_arcblock_did.types.RoleType.ROLE_ACCOUNT]: "account",
|
|
318
|
+
[_arcblock_did.types.RoleType.ROLE_APPLICATION]: "account",
|
|
319
|
+
[_arcblock_did.types.RoleType.ROLE_NODE]: "account",
|
|
320
|
+
[_arcblock_did.types.RoleType.ROLE_VALIDATOR]: "account",
|
|
321
|
+
[_arcblock_did.types.RoleType.ROLE_BOT]: "account",
|
|
322
|
+
[_arcblock_did.types.RoleType.ROLE_PASSKEY]: "account",
|
|
323
|
+
[_arcblock_did.types.RoleType.ROLE_TOKEN]: "token",
|
|
324
|
+
[_arcblock_did.types.RoleType.ROLE_ASSET]: "asset",
|
|
325
|
+
[_arcblock_did.types.RoleType.ROLE_FACTORY]: "factory",
|
|
326
|
+
[_arcblock_did.types.RoleType.ROLE_TOKEN_FACTORY]: "factory",
|
|
327
|
+
[_arcblock_did.types.RoleType.ROLE_STAKE]: "stake",
|
|
328
|
+
[_arcblock_did.types.RoleType.ROLE_ROLLUP]: "bridge",
|
|
329
|
+
[_arcblock_did.types.RoleType.ROLE_DELEGATION]: "delegate"
|
|
330
|
+
};
|
|
331
|
+
/** Extract OCAP entity type from a DID address using its encoded role byte. */
|
|
332
|
+
function resolveDidRole(address) {
|
|
333
|
+
try {
|
|
334
|
+
const info = (0, _arcblock_did.toTypeInfo)(address);
|
|
335
|
+
if (info.role === void 0) return null;
|
|
336
|
+
return ROLE_TO_ENTITY[info.role] ?? null;
|
|
337
|
+
} catch {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Granular human-readable role label for a DID (e.g., "Validator", "Bot",
|
|
343
|
+
* "Application"). Used by the account detail page's role pill — the
|
|
344
|
+
* generic resolveDidRole returns just "account" for ROLE_VALIDATOR /
|
|
345
|
+
* ROLE_BOT / ROLE_NODE / etc., losing the actual sub-type information
|
|
346
|
+
* the user sees in legacy explorers.
|
|
347
|
+
*/
|
|
348
|
+
const ROLE_TO_LABEL = {
|
|
349
|
+
[_arcblock_did.types.RoleType.ROLE_ACCOUNT]: "Account",
|
|
350
|
+
[_arcblock_did.types.RoleType.ROLE_APPLICATION]: "Application",
|
|
351
|
+
[_arcblock_did.types.RoleType.ROLE_NODE]: "Node",
|
|
352
|
+
[_arcblock_did.types.RoleType.ROLE_VALIDATOR]: "Validator",
|
|
353
|
+
[_arcblock_did.types.RoleType.ROLE_BOT]: "Bot",
|
|
354
|
+
[_arcblock_did.types.RoleType.ROLE_PASSKEY]: "Passkey",
|
|
355
|
+
[_arcblock_did.types.RoleType.ROLE_TOKEN]: "Token",
|
|
356
|
+
[_arcblock_did.types.RoleType.ROLE_ASSET]: "Asset",
|
|
357
|
+
[_arcblock_did.types.RoleType.ROLE_FACTORY]: "Factory",
|
|
358
|
+
[_arcblock_did.types.RoleType.ROLE_TOKEN_FACTORY]: "Token Factory",
|
|
359
|
+
[_arcblock_did.types.RoleType.ROLE_STAKE]: "Stake",
|
|
360
|
+
[_arcblock_did.types.RoleType.ROLE_ROLLUP]: "Rollup",
|
|
361
|
+
[_arcblock_did.types.RoleType.ROLE_DELEGATION]: "Delegation"
|
|
362
|
+
};
|
|
363
|
+
function resolveDidRoleLabel(address) {
|
|
364
|
+
try {
|
|
365
|
+
const info = (0, _arcblock_did.toTypeInfo)(address);
|
|
366
|
+
if (info.role === void 0) return null;
|
|
367
|
+
return ROLE_TO_LABEL[info.role] ?? null;
|
|
368
|
+
} catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* "transfer_v3" → "Transfer V3"
|
|
374
|
+
*
|
|
375
|
+
* Display rebrand: the chain protocol calls them "rollups" in tx type names
|
|
376
|
+
* (\`create_rollup\`, \`update_rollup\`, …) but the user-facing surface uses
|
|
377
|
+
* "Bridge". Substitute the word in the rendered label so the Type column
|
|
378
|
+
* reads "Create Bridge" / "Update Bridge" without changing the on-chain
|
|
379
|
+
* type literal that GQL still uses.
|
|
380
|
+
*/
|
|
381
|
+
function formatTxTypeName(type) {
|
|
382
|
+
return type.split("_").filter(Boolean).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ").replace(/\bRollup\b/g, "Bridge");
|
|
383
|
+
}
|
|
384
|
+
/** Map tx status code to an AUP intent string. */
|
|
385
|
+
function txStatusIntent(code) {
|
|
386
|
+
if (!code || code === "OK") return "success";
|
|
387
|
+
return "danger";
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Resolve a transaction's true sender and receiver from its nested `tx.itxJson`.
|
|
391
|
+
*
|
|
392
|
+
* v3-style (transfer_v3, acquire_asset_v3) carries an `inputs[]`/`outputs[]`
|
|
393
|
+
* payload. The true sender is `inputs[0].owner`; the outer `tx.from` holds the
|
|
394
|
+
* output owner (submitter), not the party losing tokens. Using `tx.from` as the
|
|
395
|
+
* sender makes every v3 transfer look like a self-transfer.
|
|
396
|
+
*
|
|
397
|
+
* Legacy (transfer, transfer_v2, …) has no inputs/outputs — sender comes from
|
|
398
|
+
* the outer `tx.from` and receiver from `itxJson.to` / `itxJson.receiver`.
|
|
399
|
+
*
|
|
400
|
+
* account_migrate carries the migration target in `itxJson.address` (the new
|
|
401
|
+
* DID the account is migrating to); the outer receiver is empty on chain. We
|
|
402
|
+
* surface it as the receiver so the global tx list "To" column points at the
|
|
403
|
+
* destination account instead of rendering blank.
|
|
404
|
+
*/
|
|
405
|
+
function resolveTxParties(tx) {
|
|
406
|
+
const outer = tx.tx ?? {};
|
|
407
|
+
const itxJson = outer.itxJson ?? {};
|
|
408
|
+
const inputOwner = itxJson.inputs?.[0]?.owner;
|
|
409
|
+
const outputOwner = itxJson.outputs?.[0]?.owner;
|
|
410
|
+
const nonEmpty = (v) => typeof v === "string" && v.length > 0;
|
|
411
|
+
const senderFromTop = nonEmpty(tx.sender) ? tx.sender : void 0;
|
|
412
|
+
const receiverFromTop = nonEmpty(tx.receiver) ? tx.receiver : void 0;
|
|
413
|
+
if (inputOwner || outputOwner) {
|
|
414
|
+
const sender$1 = nonEmpty(inputOwner) ? inputOwner : senderFromTop ?? (nonEmpty(outer.from) ? outer.from : void 0);
|
|
415
|
+
const receiver$1 = nonEmpty(outputOwner) ? outputOwner : receiverFromTop;
|
|
416
|
+
if (sender$1 || receiver$1) return {
|
|
417
|
+
sender: sender$1,
|
|
418
|
+
receiver: receiver$1
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
let sender = senderFromTop;
|
|
422
|
+
if (!sender && nonEmpty(outer.from)) sender = outer.from;
|
|
423
|
+
let receiver = receiverFromTop;
|
|
424
|
+
if (!receiver) {
|
|
425
|
+
if (nonEmpty(itxJson.receiver)) receiver = itxJson.receiver;
|
|
426
|
+
else if (nonEmpty(itxJson.to)) receiver = itxJson.to;
|
|
427
|
+
else if (nonEmpty(outer.receiver)) receiver = outer.receiver;
|
|
428
|
+
else if (itxJson.type_url === "fg:t:account_migrate" && nonEmpty(itxJson.address)) receiver = itxJson.address;
|
|
429
|
+
else if (typeof itxJson.type_url === "string" && STAKE_LIKE_TYPE_URLS.has(itxJson.type_url) && nonEmpty(itxJson.address)) receiver = itxJson.address;
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
sender,
|
|
433
|
+
receiver
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
const STAKE_LIKE_TYPE_URLS = new Set([
|
|
437
|
+
"fg:t:stake",
|
|
438
|
+
"fg:t:revoke_stake",
|
|
439
|
+
"fg:t:claim_stake",
|
|
440
|
+
"fg:t:return_stake",
|
|
441
|
+
"fg:t:slash_stake"
|
|
442
|
+
]);
|
|
443
|
+
/**
|
|
444
|
+
* Format a raw bignum string (possibly negative) to a human-readable decimal
|
|
445
|
+
* with a leading +/- sign. Suitable for displaying receipt change amounts.
|
|
446
|
+
*
|
|
447
|
+
* The integer part wears thousand separators so that pre-formatted strings
|
|
448
|
+
* (`_gasFeeFormatted`, `_serviceFeeFormatted`, receipt `valueFormatted`)
|
|
449
|
+
* read consistently with the browser-side `bignum` formatter (B1).
|
|
450
|
+
*
|
|
451
|
+
* Example: formatBignumValue("1000000000000000000", 18) → "+1"
|
|
452
|
+
* formatBignumValue("-500000000000000000", 18) → "-0.5"
|
|
453
|
+
* formatBignumValue("1230000000000000000000000000", 18) → "+1,230,000,000"
|
|
454
|
+
*/
|
|
455
|
+
function formatBignumValue(value, decimal) {
|
|
456
|
+
if (!value || value === "0") return "0";
|
|
457
|
+
try {
|
|
458
|
+
const negative = value.startsWith("-");
|
|
459
|
+
const padded = (negative ? value.slice(1) : value).padStart(decimal + 1, "0");
|
|
460
|
+
const intPart = padded.slice(0, padded.length - decimal) || "0";
|
|
461
|
+
const fracPart = padded.slice(padded.length - decimal).replace(/0+$/, "");
|
|
462
|
+
const intWithSep = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
463
|
+
const formatted = fracPart ? `${intWithSep}.${fracPart}` : intWithSep;
|
|
464
|
+
return negative ? `-${formatted}` : `+${formatted}`;
|
|
465
|
+
} catch {
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* B6 — Account moniker often defaults to the account's own address when the
|
|
471
|
+
* user never set a friendly name. Surfacing it in a "Name" column next to
|
|
472
|
+
* "Address" creates a duplicate-looking row. This helper strips that
|
|
473
|
+
* duplication so the column shows "—" instead.
|
|
474
|
+
*/
|
|
475
|
+
function displayMoniker(moniker, address) {
|
|
476
|
+
if (!moniker || moniker === address) return "—";
|
|
477
|
+
return moniker;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* B3 — render a numeric integer with thousand separators, replacing the
|
|
481
|
+
* sentinel value `0` with a semantic label such as "Unlimited" / "Never".
|
|
482
|
+
*
|
|
483
|
+
* Why server-side: the value the user reads ("Unlimited") and the value
|
|
484
|
+
* the renderer formats ("1,234") are two different presentations of the
|
|
485
|
+
* same field. Rather than teach the renderer about provider-specific
|
|
486
|
+
* sentinels (factory limit=0, asset ttl=0, tx serviceFee=0), the provider
|
|
487
|
+
* resolves the sentinel and ships a plain string the renderer can drop in.
|
|
488
|
+
*/
|
|
489
|
+
function formatIntegerOrSentinel(value, zeroLabel) {
|
|
490
|
+
if (value === null || value === void 0 || value === "") return zeroLabel;
|
|
491
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
492
|
+
if (!Number.isFinite(n) || n === 0) return zeroLabel;
|
|
493
|
+
return n.toLocaleString("en-US");
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Compose bridge `closed` + `paused` into a single user-facing label so the
|
|
497
|
+
* Bridges list and the bridge detail hero surface the same information.
|
|
498
|
+
*
|
|
499
|
+
* Why combined instead of "closed only" on the list: a bridge can be both
|
|
500
|
+
* closed and paused, or active-but-paused. Hiding the paused flag on the
|
|
501
|
+
* list left users wondering why an "Active" bridge wasn't accepting traffic.
|
|
502
|
+
*
|
|
503
|
+
* Output shape:
|
|
504
|
+
* - Active (closed=false, paused=false)
|
|
505
|
+
* - Active · Paused (closed=false, paused=true)
|
|
506
|
+
* - Closed (closed=true, paused=false)
|
|
507
|
+
* - Closed · Paused (closed=true, paused=true)
|
|
508
|
+
*/
|
|
509
|
+
function formatBridgeStatusLabel(closed, paused) {
|
|
510
|
+
const base = closed ? "Closed" : "Active";
|
|
511
|
+
return paused ? `${base} · Paused` : base;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Sibling color pair for the Status colored-badge in the bridges list.
|
|
515
|
+
*
|
|
516
|
+
* Mapping mirrors `formatBridgeStatusLabel`:
|
|
517
|
+
* - Closed → red (operational dead-end, not coming back)
|
|
518
|
+
* - Paused (still open) → amber (warning, may resume)
|
|
519
|
+
* - Active → green (healthy)
|
|
520
|
+
*
|
|
521
|
+
* Uses the same `{main, light}` shape the colored-badge formatter expects.
|
|
522
|
+
*/
|
|
523
|
+
function formatBridgeStatusColor(closed, paused) {
|
|
524
|
+
if (closed) return {
|
|
525
|
+
main: "#991B1B",
|
|
526
|
+
light: "#FEE2E2"
|
|
527
|
+
};
|
|
528
|
+
if (paused) return {
|
|
529
|
+
main: "#92400E",
|
|
530
|
+
light: "#FEF3C7"
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
main: "#0E7C5A",
|
|
534
|
+
light: "#E8F8F1"
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Re-export `toStakeAddress` from `@arcblock/did-util` so OCAP callers stay
|
|
539
|
+
* tied to the canonical chain primitive (single source of truth lives in
|
|
540
|
+
* `blockchain/did/did-util/src/shared.ts`).
|
|
541
|
+
*
|
|
542
|
+
* did-util's implementation uses `Buffer.concat([Buffer.from(x)])`. On
|
|
543
|
+
* Cloudflare Workers this still works because the arc worker enables
|
|
544
|
+
* `compatibility_flags = ["nodejs_compat"]` (`runtimes/cloudflare/wrangler.toml:25`),
|
|
545
|
+
* which exposes the Node-style `Buffer` global. The `worker-compat.test.ts`
|
|
546
|
+
* guard rail bans `node:*` imports from shipped provider sources, not the
|
|
547
|
+
* Node-compat globals, so this re-export is safe across Node, Bun, and
|
|
548
|
+
* workerd.
|
|
549
|
+
*
|
|
550
|
+
* Used by Bridge Slash listing to match `claim_block_reward` receipts back
|
|
551
|
+
* to the rollup's validator stake addresses.
|
|
552
|
+
*/
|
|
553
|
+
const toStakeAddress = _arcblock_did_util_cbor.toStakeAddress;
|
|
554
|
+
|
|
555
|
+
//#endregion
|
|
556
|
+
exports.COLLECTION_TO_TYPES = COLLECTION_TO_TYPES;
|
|
557
|
+
exports.TOP_LEVEL_DIRS = TOP_LEVEL_DIRS;
|
|
558
|
+
exports.TYPE_TO_COLLECTION = TYPE_TO_COLLECTION;
|
|
559
|
+
exports.buildDirEntry = buildDirEntry;
|
|
560
|
+
exports.buildListResult = buildListResult;
|
|
561
|
+
exports.collectionKind = collectionKind;
|
|
562
|
+
exports.displayMoniker = displayMoniker;
|
|
563
|
+
exports.extractAssetFilters = extractAssetFilters;
|
|
564
|
+
exports.extractBridgeFilters = extractBridgeFilters;
|
|
565
|
+
exports.extractDelegationFilters = extractDelegationFilters;
|
|
566
|
+
exports.extractFactoryFilters = extractFactoryFilters;
|
|
567
|
+
exports.extractStakeFilters = extractStakeFilters;
|
|
568
|
+
exports.extractTokenFilters = extractTokenFilters;
|
|
569
|
+
exports.extractTxFilters = extractTxFilters;
|
|
570
|
+
exports.formatBignumValue = formatBignumValue;
|
|
571
|
+
exports.formatBridgeStatusColor = formatBridgeStatusColor;
|
|
572
|
+
exports.formatBridgeStatusLabel = formatBridgeStatusLabel;
|
|
573
|
+
exports.formatIntegerOrSentinel = formatIntegerOrSentinel;
|
|
574
|
+
exports.formatTxTypeName = formatTxTypeName;
|
|
575
|
+
exports.listOpts = listOpts;
|
|
576
|
+
exports.resolveDidRole = resolveDidRole;
|
|
577
|
+
exports.resolveDidRoleLabel = resolveDidRoleLabel;
|
|
578
|
+
exports.resolveTxParties = resolveTxParties;
|
|
579
|
+
exports.toPaging = toPaging;
|
|
580
|
+
exports.toStakeAddress = toStakeAddress;
|
|
581
|
+
exports.txStatusIntent = txStatusIntent;
|