@artblocks/abx-token-api 0.1.0-alpha.5 → 0.1.0-alpha.50
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/CHANGELOG.md +155 -0
- package/dist/code.d.ts +33 -12
- package/dist/code.d.ts.map +1 -1
- package/dist/code.js +68 -56
- package/dist/code.js.map +1 -1
- package/dist/{art.d.ts → content.d.ts} +5 -5
- package/dist/content.d.ts.map +1 -0
- package/dist/{art.js → content.js} +6 -6
- package/dist/content.js.map +1 -0
- package/dist/control-plane.d.ts +24 -13
- package/dist/control-plane.d.ts.map +1 -1
- package/dist/control-plane.js +289 -43
- package/dist/control-plane.js.map +1 -1
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +55 -8
- package/dist/dashboard.js.map +1 -1
- package/dist/deps.d.ts +28 -110
- package/dist/deps.d.ts.map +1 -1
- package/dist/deps.js +34 -184
- package/dist/deps.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +87 -4
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +343 -105
- package/dist/metadata.js.map +1 -1
- package/dist/resolve.d.ts +0 -2
- package/dist/resolve.d.ts.map +1 -1
- package/dist/resolve.js +0 -5
- package/dist/resolve.js.map +1 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +257 -70
- package/dist/server.js.map +1 -1
- package/dist/watcher.d.ts +1 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +137 -8
- package/dist/watcher.js.map +1 -1
- package/package.json +9 -8
- package/dist/abxjs.d.ts +0 -13
- package/dist/abxjs.d.ts.map +0 -1
- package/dist/abxjs.js +0 -49
- package/dist/abxjs.js.map +0 -1
- package/dist/art.d.ts.map +0 -1
- package/dist/art.js.map +0 -1
- package/dist/inline.d.ts +0 -19
- package/dist/inline.d.ts.map +0 -1
- package/dist/inline.js +0 -23
- package/dist/inline.js.map +0 -1
package/dist/metadata.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fieldOf, inlineText, renderArtifactKey, stitchAttributes, verifyAgainstHash, METADATA_FIELD as F, METADATA_REPRESENTATION as R, } from '@artblocks/abx-sdk';
|
|
1
|
+
import { fieldOf, inlineText, renderArtifactKey, stitchAttributes, verifyAgainstHash, METADATA_FIELD as F, METADATA_REPRESENTATION as R, gatewayConfigFromEnv, projectGatewayPrefix, projectGatewayUrl, } from '@artblocks/abx-sdk';
|
|
2
2
|
import { contentTypeFromPath } from '@artblocks/abx-storage';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveFieldBytes, resolveFieldRendered, COLLECTION_TOKEN_ID } from './resolve.js';
|
|
4
4
|
import { currentRenderArtifact, currentSettledInputsHash, isCodeProject, liveViewEnabled, liveViewUrl } from './code.js';
|
|
5
5
|
/**
|
|
6
6
|
* Standard, marketplace-facing metadata, built from the reconstructed projection by the
|
|
@@ -11,12 +11,29 @@ import { currentRenderArtifact, currentSettledInputsHash, isCodeProject, liveVie
|
|
|
11
11
|
export function tokenImageUrl(baseUrl, chainId, address, tokenId) {
|
|
12
12
|
return `${baseUrl}/t/${chainId}/${address}/${tokenId}/image`;
|
|
13
13
|
}
|
|
14
|
+
/** How the bytes reached the document — the off-chain twin of `AbxMetadataRenderer._sourceNote`.
|
|
15
|
+
* `url`/`url-template` say "locator" because that is what the REPRESENTATION means by
|
|
16
|
+
* construction, not because anything parsed the string. */
|
|
17
|
+
const sourceNote = (rep) => {
|
|
18
|
+
if (rep === R.url)
|
|
19
|
+
return 'stored on chain; the value is a locator';
|
|
20
|
+
if (rep === R.urlTemplate)
|
|
21
|
+
return 'composed on chain from a stored template; a locator';
|
|
22
|
+
if (rep === R.renderer)
|
|
23
|
+
return 'computed on chain (field renderer)';
|
|
24
|
+
if (rep === R.inlineGzip || rep === R.readerGzip)
|
|
25
|
+
return `stored on chain (${rep} — inflated off-chain)`;
|
|
26
|
+
if (rep === R.reader)
|
|
27
|
+
return 'stored on chain (chunked)';
|
|
28
|
+
return 'stored on chain';
|
|
29
|
+
};
|
|
14
30
|
const onChainProv = (field, rep, fromCollection = false) => ({
|
|
15
31
|
field,
|
|
16
32
|
source: rep,
|
|
17
|
-
onChain: true, // the bytes live on-chain (gzip variants are just inflated off-chain on the way out)
|
|
18
33
|
status: 'on-chain',
|
|
19
|
-
|
|
34
|
+
// Mirrors AbxMetadataRenderer._sourceNote exactly — the two lanes describe the same routes in
|
|
35
|
+
// the same words, and neither claims anything about where a value RESOLVES.
|
|
36
|
+
note: scopeNote(sourceNote(rep), fromCollection),
|
|
20
37
|
});
|
|
21
38
|
/** Tag a provenance note with `[collection]` when the value came from the collection scope
|
|
22
39
|
* (a token field fell back to the contract-wide field). Mirrors the on-chain renderer. */
|
|
@@ -50,34 +67,133 @@ function offChainProv(field, entry, served) {
|
|
|
50
67
|
: verified === false
|
|
51
68
|
? `off-chain bytes; on-chain ${anchor} anchor MISMATCH`
|
|
52
69
|
: 'off-chain operator metadata; no on-chain anchor';
|
|
53
|
-
return { field, source: 'off-chain',
|
|
70
|
+
return { field, source: 'off-chain', status, anchor, note };
|
|
54
71
|
}
|
|
55
72
|
const placeholderProv = (field, note = 'placeholder — unset') => ({
|
|
56
73
|
field,
|
|
57
74
|
source: 'placeholder',
|
|
58
|
-
onChain: false,
|
|
59
75
|
status: 'n/a',
|
|
60
76
|
note,
|
|
61
77
|
});
|
|
62
|
-
/** The `name` fallback: the
|
|
63
|
-
*
|
|
64
|
-
* chain alone, so it's `on-chain` — mirrors
|
|
78
|
+
/** The `name` fallback: the contract's `name()` (on-chain storage — `CollectionMetadataLib`,
|
|
79
|
+
* shared by the 721 and 1155 bases) + tokenId. `source` is "fallback" (not from the metadata
|
|
80
|
+
* field store) but the value is reconstructable from chain alone, so it's `on-chain` — mirrors
|
|
81
|
+
* the on-chain renderer's _resolveName. */
|
|
65
82
|
const fallbackNameProv = (note) => ({
|
|
66
83
|
field: F.name,
|
|
67
84
|
source: 'fallback',
|
|
68
|
-
onChain: true,
|
|
69
85
|
status: 'on-chain',
|
|
70
86
|
note,
|
|
71
87
|
});
|
|
72
|
-
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
88
|
+
/**
|
|
89
|
+
* The `name` fallback's provenance, told truthfully about which read actually produced it.
|
|
90
|
+
*
|
|
91
|
+
* `state.name` is a value the spine only *pings*, so `reconstructProject` reads it at head — and a
|
|
92
|
+
* head read that fails is persisted as `null`, indistinguishable from a contract that genuinely has
|
|
93
|
+
* no name. The fallback then serves `state.address`, and the old note still claimed the value came
|
|
94
|
+
* from `ERC-721 name()`. A hosted node served `"name": "0xb844…c35e56 #0"` beside
|
|
95
|
+
* `note: "on-chain (ERC-721 name() + #id)"` while that contract's `name()` was `ABXdoku`; it
|
|
96
|
+
* self-corrected on the next re-index, which is the signature of the head-read theory.
|
|
97
|
+
*
|
|
98
|
+
* The wrong name is cosmetic. Provenance is the surface you'd point a creator at to audit their own
|
|
99
|
+
* metadata, so a false positive there costs more than the name does. We deliberately do NOT
|
|
100
|
+
* adjudicate *why* the read came back empty — the projection cannot tell an unnamed contract from a
|
|
101
|
+
* failed read, and guessing is how the original note came to lie.
|
|
102
|
+
*/
|
|
103
|
+
const nameFallbackProv = (named, suffix) => fallbackNameProv(named
|
|
104
|
+
? `on-chain (name()${suffix})`
|
|
105
|
+
: `on-chain (contract address${suffix}) — name() returned no value`);
|
|
106
|
+
/** Resolve a text field: on-chain content (inline/reader ±gzip, or `renderer` computed at read) →
|
|
107
|
+
* off-chain operator value → fallback default. On-chain content is decoded via the shared resolver
|
|
108
|
+
* (eth_call for `reader` and `renderer`), so a large reader-backed description resolves off-chain
|
|
109
|
+
* exactly as on-chain.
|
|
110
|
+
*
|
|
111
|
+
* `opts.uriRoute` marks a **URI-valued** field (`animation_url`): its stored bytes ARE the document,
|
|
112
|
+
* so they cannot land as the value — a marketplace iframe would get HTML where it expected a URL.
|
|
113
|
+
* The on-chain renderer has no URL space and must inline them as a `data:` URI; this node has one,
|
|
114
|
+
* so it serves the same bytes at `uriRoute` (the data plane's `/…/data/{field}` route, declared
|
|
115
|
+
* Content-Type, 302 to a durable locator when one exists) and carries the route as the value —
|
|
116
|
+
* exactly what `image` has always done for the same content, through the same kind of route.
|
|
117
|
+
*
|
|
118
|
+
* Why not the `data:` wrap here too: the value is repeated verbatim into the `artifacts` listing, so
|
|
119
|
+
* a multi-megabyte inline document shipped TWICE in a document marketplaces re-fetch on every view,
|
|
120
|
+
* while the sibling `image` — usually the smaller asset — shipped as a link. The wrap was also
|
|
121
|
+
* already the exception rather than the rule off chain: a code project's `animation_url` has always
|
|
122
|
+
* been the live-view route. A locator representation (`url`/`url-template`, or a computed
|
|
123
|
+
* `text/uri-list`) still lands verbatim — it is already a URI, and re-hosting it would hide it.
|
|
124
|
+
*
|
|
125
|
+
* `tokenAddress` is positional, not an opt, deliberately: it is only needed for the `renderer`
|
|
126
|
+
* eth_call, and an optional field that silently disables a whole representation is exactly how
|
|
127
|
+
* that branch went missing here in the first place. The compiler now makes every call site say it. */
|
|
128
|
+
async function resolveText(client, tokenAddress, fields, field, offChain, fallback,
|
|
129
|
+
// Explicit `| undefined` rather than optional: `opts` after it carries the REQUIRED gateway
|
|
130
|
+
// prefixes, and an optional parameter cannot precede a required one. Every call site already
|
|
131
|
+
// passes this positionally anyway.
|
|
132
|
+
fallbackProv, opts) {
|
|
76
133
|
const { entry, fromCollection } = fieldWithFallback(fields, opts.collectionFields, field);
|
|
77
|
-
// on-chain content (inline/reader, ±gzip) → decoded
|
|
78
|
-
const onChain = await
|
|
134
|
+
// on-chain content (inline/reader, ±gzip) → the document bytes, decoded.
|
|
135
|
+
const onChain = await resolveFieldBytes(client, entry);
|
|
79
136
|
if (onChain !== null && entry) {
|
|
80
|
-
|
|
137
|
+
// The bytes reached the document either way; `uriRoute` only decides whether they ride IN it.
|
|
138
|
+
const value = opts.uriRoute ?? new TextDecoder().decode(onChain);
|
|
139
|
+
return { value, prov: onChainProv(field, entry.representation, fromCollection) };
|
|
140
|
+
}
|
|
141
|
+
// Computed on-chain at read (`renderer`) — the field's value is a field-renderer address, so the
|
|
142
|
+
// content only exists after an eth_call. This branch mirrors {AbxMetadataRenderer._appendText}'s
|
|
143
|
+
// R_RENDERER arm **exactly**, because the two must agree: with `tokenURIRenderer` set the chain
|
|
144
|
+
// assembles this JSON and this resolver merely re-serves it, so any difference here is a resolver
|
|
145
|
+
// that contradicts the token's own tokenURI. Hence the declared contentType is used verbatim
|
|
146
|
+
// (`renderer` is the one representation that types itself on-chain) rather than assuming HTML.
|
|
147
|
+
//
|
|
148
|
+
// The `text/uri-list` exception (RFC 2483 — "this payload is a URI") carries the whole reason a
|
|
149
|
+
// URI-valued field can't just be data-wrapped: the computed bytes ARE a locator, so they land
|
|
150
|
+
// verbatim and stay dereferenceable. That is the canonical generator's directory branch.
|
|
151
|
+
let rendererFailure = null;
|
|
152
|
+
if (entry && entry.representation === R.renderer) {
|
|
153
|
+
try {
|
|
154
|
+
const rendered = await resolveFieldRendered(client, tokenAddress, opts.tokenId ?? COLLECTION_TOKEN_ID, field, entry);
|
|
155
|
+
if (rendered) {
|
|
156
|
+
const uriList = rendered.contentType === 'text/uri-list';
|
|
157
|
+
const text = new TextDecoder().decode(rendered.bytes);
|
|
158
|
+
// A computed locator lands verbatim (it IS a URI); computed CONTENT on a URI-valued field
|
|
159
|
+
// goes to the route, same as stored content above; a plain text field carries the text.
|
|
160
|
+
const value = uriList ? text.trim() : (opts.uriRoute ?? text);
|
|
161
|
+
// ONE provenance for both arms. The `text/uri-list` arm used to hand-build an object that
|
|
162
|
+
// was byte-identical to this call — same keys, same order, the same note string typed a
|
|
163
|
+
// second time — so the only thing the branch could ever do was drift from `sourceNote`.
|
|
164
|
+
return { value, prov: onChainProv(field, R.renderer, fromCollection) };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
// `IAbxFieldRenderer`'s first invariant is NEVER REVERT, but a resolver cannot rely on a
|
|
169
|
+
// third-party contract honouring it — a reverting renderer must degrade this one field, not
|
|
170
|
+
// 500 the whole token's metadata. We still fall through to the operator's off-chain value
|
|
171
|
+
// (better than nothing), but the provenance says the on-chain attempt failed rather than
|
|
172
|
+
// quietly reporting `off-chain` as if nothing were committed on-chain.
|
|
173
|
+
rendererFailure = err.message;
|
|
174
|
+
console.warn(`[metadata] field renderer for '${field}' on ${tokenAddress} reverted: ${rendererFailure}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const noteRendererFailure = (out) => rendererFailure === null
|
|
178
|
+
? out
|
|
179
|
+
: { ...out, prov: { ...out.prov, note: `${out.prov.note} — on-chain field renderer reverted, so it could not be used` } };
|
|
180
|
+
// Content-addressed locator (`ipfs`/`arweave`): the CID/txid is identity, the collection's
|
|
181
|
+
// preferred gateway is what makes it dereferenceable. Mirrors {AbxMetadataRenderer._appendText}'s
|
|
182
|
+
// v11 arm. A value that locates nothing (`ipfs://` with no CID) falls through and omits, rather
|
|
183
|
+
// than emitting a bare gateway prefix that 404s.
|
|
184
|
+
if (entry && (entry.representation === R.ipfs || entry.representation === R.arweave)) {
|
|
185
|
+
const wrapped = gatewayFieldUrl(entry, opts.gateways, opts.tokenId === COLLECTION_TOKEN_ID ? undefined : opts.tokenId);
|
|
186
|
+
if (wrapped !== null) {
|
|
187
|
+
return {
|
|
188
|
+
value: wrapped,
|
|
189
|
+
prov: {
|
|
190
|
+
field,
|
|
191
|
+
source: entry.representation,
|
|
192
|
+
status: 'on-chain',
|
|
193
|
+
note: scopeNote(GATEWAY_SOURCE_NOTE, fromCollection),
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
81
197
|
}
|
|
82
198
|
// on-chain locator carried AS the text value (url, or url-template with {id} substituted).
|
|
83
199
|
if (entry && (entry.representation === R.url || entry.representation === R.urlTemplate)) {
|
|
@@ -88,19 +204,48 @@ async function resolveText(client, fields, field, offChain, fallback, fallbackPr
|
|
|
88
204
|
prov: {
|
|
89
205
|
field,
|
|
90
206
|
source: entry.representation,
|
|
91
|
-
onChain: true,
|
|
92
207
|
status: 'on-chain',
|
|
93
|
-
note: scopeNote(
|
|
208
|
+
note: scopeNote('stored on chain; the value is a locator', fromCollection),
|
|
94
209
|
},
|
|
95
210
|
};
|
|
96
211
|
}
|
|
97
212
|
if (offChain != null) {
|
|
98
|
-
return { value: offChain, prov: offChainProv(field, entry, offChain) };
|
|
213
|
+
return noteRendererFailure({ value: offChain, prov: offChainProv(field, entry, offChain) });
|
|
99
214
|
}
|
|
100
215
|
if (fallback != null) {
|
|
101
|
-
return {
|
|
216
|
+
return noteRendererFailure({
|
|
217
|
+
value: fallback,
|
|
218
|
+
prov: fallbackProv ?? placeholderProv(field, 'default (no on-chain or off-chain value)'),
|
|
219
|
+
});
|
|
102
220
|
}
|
|
103
|
-
return { value: null, prov: placeholderProv(field) };
|
|
221
|
+
return noteRendererFailure({ value: null, prov: placeholderProv(field) });
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The project's preferred gateway prefix per scheme, resolved ONCE per document.
|
|
225
|
+
*
|
|
226
|
+
* The off-chain twin of `AbxMetadataRenderer._gatewayPrefix` (spec v11): the collection's reserved
|
|
227
|
+
* `abx_gateway_ipfs` / `abx_gateway_arweave` field, else this host's env default, else the public
|
|
228
|
+
* floor. Two conforming resolvers given the same chain must emit the same `image`, so the env read
|
|
229
|
+
* is a FLOOR and never an override — a project that stated a preference on chain gets that
|
|
230
|
+
* preference from every resolver, and a host's own gateway only ever fills a silence (which is the
|
|
231
|
+
* only way a managed provider serving a token it does not own can offer a better default).
|
|
232
|
+
*/
|
|
233
|
+
export function projectGateways(state) {
|
|
234
|
+
const env = gatewayConfigFromEnv();
|
|
235
|
+
return {
|
|
236
|
+
ipfs: projectGatewayPrefix(state, 'ipfs', env),
|
|
237
|
+
arweave: projectGatewayPrefix(state, 'arweave', env),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/** The provenance note for a content-addressed locator. Byte-identical to the deployed renderer's
|
|
241
|
+
* `_sourceNote` for `ipfs`/`arweave` — deliberately "served through", never "verified": the
|
|
242
|
+
* locator is on chain, the bytes are not, and nothing here re-hashes what a gateway returns. */
|
|
243
|
+
const GATEWAY_SOURCE_NOTE = "stored on chain; a content-addressed locator, served through the collection's preferred gateway";
|
|
244
|
+
/** Project a content-addressed field value the way the deployed renderer does, or `null` when the
|
|
245
|
+
* value locates nothing. `tokenId` omitted ⇒ no `{id}` substitution (the collection surface). */
|
|
246
|
+
function gatewayFieldUrl(entry, gateways, tokenId) {
|
|
247
|
+
const network = entry.representation === R.ipfs ? 'ipfs' : 'arweave';
|
|
248
|
+
return projectGatewayUrl(network, inlineText(entry).trim(), gateways[network], tokenId);
|
|
104
249
|
}
|
|
105
250
|
/**
|
|
106
251
|
* Map a locator to the provenance `source` describing where the bytes actually live. Locators are
|
|
@@ -125,7 +270,10 @@ function sourceForLocator(locator) {
|
|
|
125
270
|
* one (bridged or from the active backend) so the heavy asset resolves peer-to-peer; else
|
|
126
271
|
* this node's `/…/image` route serves it from custody. Either way it's `anchored` by the
|
|
127
272
|
* on-chain hash (verify via `/verify`).
|
|
128
|
-
* -
|
|
273
|
+
* - content-addressed on-chain (`ipfs`/`arweave`) → the CID/txid wrapped in the collection's
|
|
274
|
+
* preferred gateway prefix, so what lands in `image` is an `https://` a wallet can render;
|
|
275
|
+
* `on-chain`.
|
|
276
|
+
* - locator on-chain (`url`) → the on-chain pointer IS the URL; `on-chain`.
|
|
129
277
|
* - unset → this node's `/…/image` route serves the deterministic generative placeholder.
|
|
130
278
|
*/
|
|
131
279
|
async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
@@ -151,7 +299,6 @@ async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
|
151
299
|
prov: {
|
|
152
300
|
field: F.image,
|
|
153
301
|
source: 'url-template',
|
|
154
|
-
onChain: true,
|
|
155
302
|
status: 'on-chain',
|
|
156
303
|
note: scopeNote('on-chain url template -> off-chain content', fromCollection),
|
|
157
304
|
},
|
|
@@ -168,7 +315,6 @@ async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
|
168
315
|
prov: {
|
|
169
316
|
field: F.image,
|
|
170
317
|
source: sourceForLocator(locator),
|
|
171
|
-
onChain: false,
|
|
172
318
|
status: 'anchored',
|
|
173
319
|
anchor,
|
|
174
320
|
note: `off-chain bytes (${sourceForLocator(locator)}); on-chain ${anchor} integrity anchor`,
|
|
@@ -181,7 +327,6 @@ async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
|
181
327
|
prov: {
|
|
182
328
|
field: F.image,
|
|
183
329
|
source: anchor,
|
|
184
|
-
onChain: false,
|
|
185
330
|
status: 'anchored',
|
|
186
331
|
anchor,
|
|
187
332
|
note: `off-chain bytes (served by this node); on-chain ${anchor} integrity anchor`,
|
|
@@ -189,33 +334,47 @@ async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
|
189
334
|
};
|
|
190
335
|
}
|
|
191
336
|
// Computed on-chain (`renderer`): the value is a field-renderer address, not a locator — point
|
|
192
|
-
// at this node's route, which eth_calls the renderer and serves the computed bytes
|
|
337
|
+
// at this node's route, which eth_calls the renderer and serves the computed bytes (302-ing to the
|
|
338
|
+
// locator if the computed content turns out to BE one). The provenance is the shared one: the route
|
|
339
|
+
// the bytes took is identical to any other on-chain content, and only the served `image` VALUE is
|
|
340
|
+
// this field's documented exception. A hand-built note here drifted from the deployed renderer's
|
|
341
|
+
// `_sourceNote` wording for the same representation.
|
|
193
342
|
if (entry.representation === R.renderer) {
|
|
194
|
-
return {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
343
|
+
return { url: nodeUrl, prov: onChainProv(F.image, R.renderer, fromCollection) };
|
|
344
|
+
}
|
|
345
|
+
// Content-addressed ON-CHAIN (ipfs/arweave): the value is identity, not a URL. Wrap it in the
|
|
346
|
+
// collection's preferred gateway — the twin of {AbxMetadataRenderer._resolveImage}'s v11 arm.
|
|
347
|
+
// Before v11 both planes emitted the raw `ipfs://…` here, which no marketplace dereferences, and
|
|
348
|
+
// `--onchain-uri --backend ipfs` worked around it by baking a gateway host into a `url` field.
|
|
349
|
+
if (entry.representation === R.ipfs || entry.representation === R.arweave) {
|
|
350
|
+
const wrapped = gatewayFieldUrl(entry, projectGateways(state), token.tokenId);
|
|
351
|
+
if (wrapped !== null) {
|
|
352
|
+
return {
|
|
353
|
+
url: wrapped,
|
|
354
|
+
prov: {
|
|
355
|
+
field: F.image,
|
|
356
|
+
source: entry.representation,
|
|
357
|
+
status: 'on-chain',
|
|
358
|
+
note: scopeNote(GATEWAY_SOURCE_NOTE, fromCollection),
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// locates nothing → fall through to the node's placeholder route rather than a bare prefix
|
|
363
|
+
return { url: nodeUrl, prov: placeholderProv(F.image, 'placeholder — the committed locator names no content') };
|
|
204
364
|
}
|
|
205
|
-
// Locator committed ON-CHAIN (
|
|
365
|
+
// Locator committed ON-CHAIN (url): the on-chain value is itself the address.
|
|
206
366
|
const onChainLocator = inlineText(entry).trim();
|
|
207
367
|
return {
|
|
208
368
|
url: onChainLocator || nodeUrl,
|
|
209
369
|
prov: {
|
|
210
370
|
field: F.image,
|
|
211
371
|
source: entry.representation,
|
|
212
|
-
onChain: true,
|
|
213
372
|
status: 'on-chain',
|
|
214
373
|
note: scopeNote(`on-chain pointer (${entry.representation}) — bytes off-chain, located on-chain`, fromCollection),
|
|
215
374
|
},
|
|
216
375
|
};
|
|
217
376
|
}
|
|
218
|
-
// ── the `artifacts` manifest (
|
|
377
|
+
// ── the `artifacts` manifest (site/content/docs/protocol/data-plane.mdx) ───────────────────────
|
|
219
378
|
/** The registry's reserved field vocabulary — projection/display keys. Every other field tag a
|
|
220
379
|
* creator sets is a first-class plane artifact. */
|
|
221
380
|
const RESERVED_FIELDS = new Set(Object.values(F));
|
|
@@ -250,7 +409,11 @@ export async function fieldMimeType(client, state, entry, field, tokenId, displa
|
|
|
250
409
|
return (await rendererContentType(client, state.address, entry, field, tokenId)) ?? 'application/octet-stream';
|
|
251
410
|
}
|
|
252
411
|
if (rep === R.inline || rep === R.inlineGzip || rep === R.reader || rep === R.readerGzip) {
|
|
253
|
-
|
|
412
|
+
// registry conventions for on-chain content bytes: an `image` is SVG, an `animation_url` is
|
|
413
|
+
// the HTML document the renderer wraps as `data:text/html` (spec v4). Anything else: the floor.
|
|
414
|
+
if (field === F.image)
|
|
415
|
+
return 'image/svg+xml';
|
|
416
|
+
return field === F.animationUrl ? 'text/html' : 'application/octet-stream';
|
|
254
417
|
}
|
|
255
418
|
if (rep === R.keccak256 || rep === R.sha256) {
|
|
256
419
|
const stored = storage ? await storage.get(entry.value).catch(() => null) : null;
|
|
@@ -260,15 +423,21 @@ export async function fieldMimeType(client, state, entry, field, tokenId, displa
|
|
|
260
423
|
return bridged ? contentTypeFromPath(bridged) : 'application/octet-stream';
|
|
261
424
|
}
|
|
262
425
|
const raw = inlineText(entry).trim();
|
|
426
|
+
// For ipfs/arweave the extension (if any) rides the stored value — no need to wrap first, and
|
|
427
|
+
// wrapping would only add a gateway host that never carries the extension.
|
|
263
428
|
return contentTypeFromPath(rep === R.urlTemplate ? applyTemplate(raw, tokenId) : raw);
|
|
264
429
|
}
|
|
265
430
|
/** A field artifact's `uri`: locator forms verbatim (content-addressed preferred, `{id}`
|
|
266
431
|
* substituted); custody-hash forms prefer the bridged durable locator; everything this node
|
|
267
432
|
* serves itself (inline/reader/renderer) rides the given `/data/{field}` route. Total — every
|
|
268
433
|
* representation resolves to some uri (the complete-listing rule). */
|
|
269
|
-
function fieldArtifactUri(entry, dataRoute, tokenId, display) {
|
|
434
|
+
function fieldArtifactUri(entry, dataRoute, tokenId, display, gateways) {
|
|
270
435
|
const rep = entry.representation;
|
|
271
|
-
|
|
436
|
+
// Content-addressed forms carry the same gateway wrap the projected fields get, so a manifest
|
|
437
|
+
// entry and the `image` it duplicates never point at two different URLs for the same bytes.
|
|
438
|
+
if (rep === R.ipfs || rep === R.arweave)
|
|
439
|
+
return gatewayFieldUrl(entry, gateways, tokenId) ?? dataRoute;
|
|
440
|
+
if (rep === R.url)
|
|
272
441
|
return inlineText(entry).trim() || dataRoute;
|
|
273
442
|
if (rep === R.urlTemplate)
|
|
274
443
|
return applyTemplate(inlineText(entry), tokenId);
|
|
@@ -283,21 +452,17 @@ function artifactFieldProv(field, entry, fromCollection) {
|
|
|
283
452
|
if (rep === R.inline || rep === R.inlineGzip || rep === R.reader || rep === R.readerGzip) {
|
|
284
453
|
return onChainProv(field, rep, fromCollection);
|
|
285
454
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
note: scopeNote('on-chain field renderer (computed from chain state at read)', fromCollection),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
455
|
+
// Same call as the branch above — `renderer` is on-chain content like `inline`/`reader`, and the
|
|
456
|
+
// note belongs to `sourceNote` (which mirrors the deployed renderer's `_sourceNote` verbatim). This
|
|
457
|
+
// branch used to hand-build the object with a longer note of its own, so one function delegated in
|
|
458
|
+
// one arm and duplicated in the next.
|
|
459
|
+
if (rep === R.renderer)
|
|
460
|
+
return onChainProv(field, rep, fromCollection);
|
|
295
461
|
if (rep === R.keccak256 || rep === R.sha256) {
|
|
296
462
|
const anchor = rep;
|
|
297
463
|
return {
|
|
298
464
|
field,
|
|
299
465
|
source: anchor,
|
|
300
|
-
onChain: false,
|
|
301
466
|
status: 'anchored',
|
|
302
467
|
anchor,
|
|
303
468
|
note: `off-chain bytes; on-chain ${anchor} integrity anchor`,
|
|
@@ -306,7 +471,6 @@ function artifactFieldProv(field, entry, fromCollection) {
|
|
|
306
471
|
return {
|
|
307
472
|
field,
|
|
308
473
|
source: rep,
|
|
309
|
-
onChain: true,
|
|
310
474
|
status: 'on-chain',
|
|
311
475
|
note: scopeNote(`on-chain pointer (${rep}) — bytes off-chain, located on-chain`, fromCollection),
|
|
312
476
|
};
|
|
@@ -352,7 +516,7 @@ async function buildTokenArtifacts(client, state, token, baseUrl, chainId, displ
|
|
|
352
516
|
entries.push({
|
|
353
517
|
key: name,
|
|
354
518
|
mimeType: await fieldMimeType(client, state, entry, name, token.tokenId, display, storage),
|
|
355
|
-
uri: fieldArtifactUri(entry, `${baseUrl}/t/${chainId}/${state.address}/${token.tokenId}/data/${name}`, token.tokenId, display),
|
|
519
|
+
uri: fieldArtifactUri(entry, `${baseUrl}/t/${chainId}/${state.address}/${token.tokenId}/data/${name}`, token.tokenId, display, projectGateways(state)),
|
|
356
520
|
});
|
|
357
521
|
prov.push(artifactFieldProv(name, entry, fromCollection));
|
|
358
522
|
}
|
|
@@ -375,7 +539,6 @@ async function buildTokenArtifacts(client, state, token, baseUrl, chainId, displ
|
|
|
375
539
|
prov.push({
|
|
376
540
|
field: key,
|
|
377
541
|
source: `effect:${row.effectKey}`,
|
|
378
|
-
onChain: false,
|
|
379
542
|
status: 'off-chain',
|
|
380
543
|
note: 'derived effect output at the current inputsHash (re-creatable bytes)',
|
|
381
544
|
});
|
|
@@ -383,6 +546,51 @@ async function buildTokenArtifacts(client, state, token, baseUrl, chainId, displ
|
|
|
383
546
|
}
|
|
384
547
|
return { entries, prov };
|
|
385
548
|
}
|
|
549
|
+
export async function tokenArtifacts(client, state, token, baseUrl, chainId, display = {}, storage, plane) {
|
|
550
|
+
const f = token.fields;
|
|
551
|
+
const cf = state.collectionFields;
|
|
552
|
+
const opts = { collectionFields: cf, tokenId: token.tokenId, gateways: projectGateways(state) };
|
|
553
|
+
// Mirrors buildTokenMetadata's own field resolution exactly (same helpers, same call shape),
|
|
554
|
+
// purely so `buildTokenArtifacts` receives the identical `resolved` shape it gets from the real
|
|
555
|
+
// document build — reusing the filter itself rather than re-deriving it.
|
|
556
|
+
const image = await resolveImage(state, token, baseUrl, chainId, display, storage);
|
|
557
|
+
const animation = await resolveText(client, state.address, f, F.animationUrl, undefined, null, undefined, {
|
|
558
|
+
...opts,
|
|
559
|
+
uriRoute: `${baseUrl}/t/${chainId}/${state.address}/${token.tokenId}/data/${F.animationUrl}`,
|
|
560
|
+
});
|
|
561
|
+
const currentInputsHash = isCodeProject(state) ? await currentSettledInputsHash(client, state, token).catch(() => null) : null;
|
|
562
|
+
const { entries, prov } = await buildTokenArtifacts(client, state, token, baseUrl, chainId, display, storage, plane, {
|
|
563
|
+
imageUrl: image.url,
|
|
564
|
+
imageEntry: fieldWithFallback(f, cf, F.image).entry,
|
|
565
|
+
animationValue: animation.value,
|
|
566
|
+
animationEntry: animation.value !== null ? fieldWithFallback(f, cf, F.animationUrl).entry : null,
|
|
567
|
+
settledHash: currentInputsHash,
|
|
568
|
+
});
|
|
569
|
+
// The visibility this read ADDS over the manifest: every registered row, current or stale, each
|
|
570
|
+
// labeled. `plane.list` is unfiltered — the manifest's own currency filter runs separately, above
|
|
571
|
+
// via `buildTokenArtifacts` — so recompute each row's OWN current-key match here rather than
|
|
572
|
+
// trusting the manifest's inclusion, which already dropped anything stale.
|
|
573
|
+
const effects = [];
|
|
574
|
+
if (plane && currentInputsHash) {
|
|
575
|
+
const rows = [...plane.list(state.address, token.tokenId)].sort((a, b) => `${a.effectKey}/${a.outputKey}`.localeCompare(`${b.effectKey}/${b.outputKey}`));
|
|
576
|
+
for (const row of rows) {
|
|
577
|
+
const key = `${row.effectKey}/${row.outputKey}`;
|
|
578
|
+
// `state.chainId` (the project's OWN chain), not the `chainId` param (which only shapes served
|
|
579
|
+
// URLs) — matching `buildTokenArtifacts`'/`currentRenderArtifact`'s own key computation exactly.
|
|
580
|
+
const expected = renderArtifactKey(state.chainId, state.address, token.tokenId, currentInputsHash, row.outputKey, row.effectKey);
|
|
581
|
+
effects.push({
|
|
582
|
+
key,
|
|
583
|
+
effectKey: row.effectKey,
|
|
584
|
+
outputKey: row.outputKey,
|
|
585
|
+
status: row.key.toLowerCase() === expected.toLowerCase() ? 'current' : 'stale',
|
|
586
|
+
inputsHash: row.inputsHash ?? null,
|
|
587
|
+
contentType: row.contentType,
|
|
588
|
+
uri: row.locator ?? `${baseUrl}/t/${chainId}/${state.address}/${token.tokenId}/data/${key}`,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return { entries, prov, effects, planeConsulted: !!plane, currentInputsHash };
|
|
593
|
+
}
|
|
386
594
|
const looksLikeSvg = (s) => /^\s*<(\?xml|svg)/i.test(s);
|
|
387
595
|
/** On-chain `attributes` inline-JSON (if any) + the operator's off-chain attributes, stitched
|
|
388
596
|
* (on-chain wins per trait_type). Returns the merged array + a provenance row, or null when
|
|
@@ -408,7 +616,6 @@ function resolveAttributes(fields, offChain, collectionFields, fromRender = fals
|
|
|
408
616
|
prov = {
|
|
409
617
|
field: F.attributes,
|
|
410
618
|
source: 'inline',
|
|
411
|
-
onChain: true,
|
|
412
619
|
status: 'on-chain',
|
|
413
620
|
note: fromRender
|
|
414
621
|
? 'on-chain inline JSON + render-effect traits (+ operator), stitched (on-chain wins per trait_type)'
|
|
@@ -425,7 +632,6 @@ function resolveAttributes(fields, offChain, collectionFields, fromRender = fals
|
|
|
425
632
|
prov = {
|
|
426
633
|
field: F.attributes,
|
|
427
634
|
source: fromRender ? 'effect:render' : 'off-chain',
|
|
428
|
-
onChain: false,
|
|
429
635
|
status: 'off-chain',
|
|
430
636
|
note: fromRender
|
|
431
637
|
? 'script-reported traits captured at render (+ operator traits; render wins per trait_type)'
|
|
@@ -434,7 +640,8 @@ function resolveAttributes(fields, offChain, collectionFields, fromRender = fals
|
|
|
434
640
|
}
|
|
435
641
|
return { attributes: merged, prov };
|
|
436
642
|
}
|
|
437
|
-
/**
|
|
643
|
+
/** Token metadata JSON — the `tokenURI` target (ERC-721) / the `uri(id)` target (ERC-1155
|
|
644
|
+
* editions), assembled identically either way + `abx_provenance` + `artifacts`. */
|
|
438
645
|
export async function buildTokenMetadata(client, state, token, baseUrl, chainId, display = {}, storage,
|
|
439
646
|
// The effect-artifact registry read surface: rows a producer PUBLISHED to this resolver (or
|
|
440
647
|
// recorded co-located). Feeds the `artifacts` manifest and the image seam's locator check —
|
|
@@ -463,7 +670,6 @@ plane) {
|
|
|
463
670
|
image.prov = {
|
|
464
671
|
field: F.image,
|
|
465
672
|
source: 'effect:render',
|
|
466
|
-
onChain: false,
|
|
467
673
|
status: 'off-chain',
|
|
468
674
|
note: viaLocator
|
|
469
675
|
? 'render effect output at the current inputsHash, published as a durable locator (resolver 302-redirects)'
|
|
@@ -484,12 +690,12 @@ plane) {
|
|
|
484
690
|
// on-chain renderer's `_field`), and url-template fields substitute this token's id.
|
|
485
691
|
const cf = state.collectionFields;
|
|
486
692
|
const tid = token.tokenId;
|
|
487
|
-
const opts = { collectionFields: cf, tokenId: tid };
|
|
488
|
-
const name = await resolveText(client, f, F.name, undefined, `${state.name ?? state.address} #${token.tokenId}`,
|
|
489
|
-
const description = await resolveText(client, f, F.description, display.description, null, undefined, opts); // optional → omit if unset
|
|
490
|
-
const externalUrl = await resolveText(client, f, F.externalUrl, display.externalUrl, null, undefined, opts); // optional → omit if unset
|
|
491
|
-
const backgroundColor = await resolveText(client, f, F.backgroundColor, undefined, null, undefined, opts);
|
|
492
|
-
const youtubeUrl = await resolveText(client, f, F.youtubeUrl, undefined, null, undefined, opts);
|
|
693
|
+
const opts = { collectionFields: cf, tokenId: tid, gateways: projectGateways(state) };
|
|
694
|
+
const name = await resolveText(client, state.address, f, F.name, undefined, `${state.name ?? state.address} #${token.tokenId}`, nameFallbackProv(state.name != null, ' + #id'), opts);
|
|
695
|
+
const description = await resolveText(client, state.address, f, F.description, display.description, null, undefined, opts); // optional → omit if unset
|
|
696
|
+
const externalUrl = await resolveText(client, state.address, f, F.externalUrl, display.externalUrl, null, undefined, opts); // optional → omit if unset
|
|
697
|
+
const backgroundColor = await resolveText(client, state.address, f, F.backgroundColor, undefined, null, undefined, opts);
|
|
698
|
+
const youtubeUrl = await resolveText(client, state.address, f, F.youtubeUrl, undefined, null, undefined, opts);
|
|
493
699
|
provenance.push(name.prov); // name is required — always present
|
|
494
700
|
// image: required — always served via our URL; image_data carries inline SVG if on-chain SVG.
|
|
495
701
|
const img = fieldOf(f, F.image);
|
|
@@ -501,8 +707,13 @@ plane) {
|
|
|
501
707
|
if (externalUrl.value !== null)
|
|
502
708
|
provenance.push(externalUrl.prov);
|
|
503
709
|
// animation_url: an explicit `animation` field wins; else a code project derives the
|
|
504
|
-
// live-view route (suppressible via the `display.animation = none` contract param).
|
|
505
|
-
|
|
710
|
+
// live-view route (suppressible via the `display.animation = none` contract param). On-chain
|
|
711
|
+
// content is served at this node's data route rather than inlined (see `uriRoute`); locator forms
|
|
712
|
+
// pass through untouched. The on-chain renderer still inlines it — it has no route to offer.
|
|
713
|
+
const animation = await resolveText(client, state.address, f, F.animationUrl, undefined, null, undefined, {
|
|
714
|
+
...opts,
|
|
715
|
+
uriRoute: `${baseUrl}/t/${chainId}/${state.address}/${token.tokenId}/data/${F.animationUrl}`,
|
|
716
|
+
});
|
|
506
717
|
let animationValue = animation.value;
|
|
507
718
|
let animationProv = animation.prov;
|
|
508
719
|
if (animationValue === null && liveViewEnabled(state)) {
|
|
@@ -510,7 +721,6 @@ plane) {
|
|
|
510
721
|
animationProv = {
|
|
511
722
|
field: F.animationUrl,
|
|
512
723
|
source: 'live-view',
|
|
513
|
-
onChain: false,
|
|
514
724
|
status: 'off-chain',
|
|
515
725
|
note: 'live view derived from the on-chain code (canonical tokenData injected at load)',
|
|
516
726
|
};
|
|
@@ -522,22 +732,27 @@ plane) {
|
|
|
522
732
|
// traits (captured at the current inputsHash), which win over the operator's off-chain
|
|
523
733
|
// traits. ABX facts (version, canonical, royalty) are NOT traits; they live in this
|
|
524
734
|
// provenance block / ERC-2981, never in the marketplace trait array.
|
|
735
|
+
// `traits` is a BOUND output (`effects.md → Bound vs referenced`): its content is held by this
|
|
736
|
+
// node, so there are exactly two places it can be, and BOTH are hash-gated by the key itself —
|
|
737
|
+
// a param change re-addresses the key, so stale traits go unstitched rather than being attributed
|
|
738
|
+
// to a state they don't depict.
|
|
739
|
+
// - the registry row (a producer REGISTERED it over /v1/effect-artifacts: bytes ride the row),
|
|
740
|
+
// - this node's storage (a CO-LOCATED runner wrote the artifact to the shared backend).
|
|
525
741
|
let renderTraits = null;
|
|
526
|
-
if (isCodeProject(state) && storage) {
|
|
742
|
+
if (isCodeProject(state) && (plane || storage)) {
|
|
527
743
|
try {
|
|
528
|
-
const { key, found } = await currentRenderArtifact(client, state, token, storage, 'traits', {
|
|
744
|
+
const { key, found } = await currentRenderArtifact(client, state, token, storage ?? undefined, 'traits', {
|
|
529
745
|
hash: settledHash ?? undefined,
|
|
530
746
|
});
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
747
|
+
const boundBytes = plane?.get(key)?.bytes ?? null;
|
|
748
|
+
const raw = boundBytes ?? (found ? (await storage?.get(key))?.bytes ?? null : null);
|
|
749
|
+
const parsed = raw ? JSON.parse(new TextDecoder().decode(raw)) : null;
|
|
750
|
+
// scripts report either the OpenSea array or the natural object form
|
|
751
|
+
// (`abx.traits({Palette: 'Dusk'})`) — normalize the latter.
|
|
752
|
+
if (Array.isArray(parsed))
|
|
753
|
+
renderTraits = parsed;
|
|
754
|
+
else if (parsed && typeof parsed === 'object') {
|
|
755
|
+
renderTraits = Object.entries(parsed).map(([trait_type, value]) => ({ trait_type, value: value }));
|
|
541
756
|
}
|
|
542
757
|
}
|
|
543
758
|
catch {
|
|
@@ -555,6 +770,13 @@ plane) {
|
|
|
555
770
|
provenance.push(backgroundColor.prov);
|
|
556
771
|
if (youtubeUrl.value)
|
|
557
772
|
provenance.push(youtubeUrl.prov);
|
|
773
|
+
// NO `abx_params` block. It was emitted here and by the on-chain renderer and read back by
|
|
774
|
+
// nobody — the only consumers in the repo were its own tests. Params already enumerate directly
|
|
775
|
+
// from the contract (`tokenParamKeys` / `tokenParam` / `paramSchemaKeys`), which is canonical and
|
|
776
|
+
// needs no indexer, and a code project's script gets them through `tokenData`. So the projection
|
|
777
|
+
// was a third serialization of data available two better ways: uncapped in size, quadratic to
|
|
778
|
+
// build, and duplicated across two implementations — which is precisely the sibling drift that
|
|
779
|
+
// produced the computed-image-locator bug. Traits that belong to marketplaces go in `attributes`.
|
|
558
780
|
// the `artifacts` manifest — the plane's complete listing (data-plane.md). Omitted when empty.
|
|
559
781
|
const artifacts = await buildTokenArtifacts(client, state, token, baseUrl, chainId, display, storage, plane, {
|
|
560
782
|
imageUrl: image.url,
|
|
@@ -592,20 +814,24 @@ export async function buildContractMetadata(client, state, baseUrl, chainId, dis
|
|
|
592
814
|
const f = state.collectionFields;
|
|
593
815
|
const provenance = [];
|
|
594
816
|
// Representative image: the first token that actually carries an `image` field, else the
|
|
595
|
-
// first token — so "first with an image" surfaces a real
|
|
817
|
+
// first token — so "first with an image" surfaces a real work rather than a placeholder.
|
|
596
818
|
const rep = state.tokens.find((t) => fieldOf(t.fields, F.image)) ?? state.tokens[0];
|
|
597
819
|
const image = rep ? await resolveImage(state, rep, baseUrl, chainId, display, storage) : undefined;
|
|
598
|
-
//
|
|
820
|
+
// The collection surface has no token, so a `renderer`-represented field is called with the
|
|
821
|
+
// sentinel id — the same one {AbxMetadataRenderer} passes for its collection reads, so a field
|
|
822
|
+
// renderer that serves both scopes sees an identical call from chain and from here.
|
|
823
|
+
const collOpts = { tokenId: COLLECTION_TOKEN_ID, gateways: projectGateways(state) };
|
|
824
|
+
// name is required (fallback = the on-chain collection name); description + external_link
|
|
599
825
|
// are optional → omit when neither on-chain nor operator-supplied (no boilerplate).
|
|
600
|
-
const name = await resolveText(client, f, F.name, undefined, state.name ?? state.address,
|
|
601
|
-
const description = await resolveText(client, f, F.description, display.description, null);
|
|
602
|
-
const externalLink = await resolveText(client, f, F.externalLink, display.externalUrl, null);
|
|
826
|
+
const name = await resolveText(client, state.address, f, F.name, undefined, state.name ?? state.address, nameFallbackProv(state.name != null, ''), collOpts);
|
|
827
|
+
const description = await resolveText(client, state.address, f, F.description, display.description, null, undefined, collOpts);
|
|
828
|
+
const externalLink = await resolveText(client, state.address, f, F.externalLink, display.externalUrl, null, undefined, collOpts);
|
|
603
829
|
// authorship + rights (collection scope) — reserved fields, on-chain-only (no operator source):
|
|
604
830
|
// included only when the creator set them on-chain, omitted otherwise (no boilerplate).
|
|
605
|
-
const
|
|
606
|
-
const displayNotes = await resolveText(client, f, F.displayNotes, undefined, null);
|
|
607
|
-
const
|
|
608
|
-
const license = await resolveText(client, f, F.license, undefined, null);
|
|
831
|
+
const creator = await resolveText(client, state.address, f, F.creator, undefined, null, undefined, collOpts);
|
|
832
|
+
const displayNotes = await resolveText(client, state.address, f, F.displayNotes, undefined, null, undefined, collOpts);
|
|
833
|
+
const creatorLinks = await resolveText(client, state.address, f, F.creatorLinks, undefined, null, undefined, collOpts);
|
|
834
|
+
const license = await resolveText(client, state.address, f, F.license, undefined, null, undefined, collOpts);
|
|
609
835
|
provenance.push(name.prov);
|
|
610
836
|
if (image)
|
|
611
837
|
provenance.push(image.prov);
|
|
@@ -613,18 +839,28 @@ export async function buildContractMetadata(client, state, baseUrl, chainId, dis
|
|
|
613
839
|
provenance.push(description.prov);
|
|
614
840
|
if (externalLink.value !== null)
|
|
615
841
|
provenance.push(externalLink.prov);
|
|
616
|
-
if (
|
|
617
|
-
provenance.push(
|
|
842
|
+
if (creator.value !== null)
|
|
843
|
+
provenance.push(creator.prov);
|
|
618
844
|
if (displayNotes.value !== null)
|
|
619
845
|
provenance.push(displayNotes.prov);
|
|
620
|
-
if (
|
|
621
|
-
provenance.push(
|
|
846
|
+
if (creatorLinks.value !== null)
|
|
847
|
+
provenance.push(creatorLinks.prov);
|
|
622
848
|
if (license.value !== null)
|
|
623
849
|
provenance.push(license.prov);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
850
|
+
// The two reserved collection IMAGE keys. Both are top-level here, and both are top-level on the
|
|
851
|
+
// on-chain renderer as of spec v10 — the point of the three-list table in
|
|
852
|
+
// site/content/docs/protocol/metadata.mdx is that a key BOTH planes can emit must not be emitted by only
|
|
853
|
+
// one of them. Two things were wrong before v10: `featured_image` appeared in `artifacts` alone
|
|
854
|
+
// (reserved, so filtered out of the top level, yet still listed — "reserved but only in artifacts",
|
|
855
|
+
// which is neither), and `banner_image` was top-level only for `representation === url`, so an
|
|
856
|
+
// `inline` or `reader` banner silently dropped even though both resolve fine. Every chain-reachable
|
|
857
|
+
// representation now counts, matching `_appendContractText` on the renderer.
|
|
858
|
+
const bannerImage = await resolveText(client, state.address, f, F.bannerImage, undefined, null, undefined, collOpts);
|
|
859
|
+
const featuredImage = await resolveText(client, state.address, f, F.featuredImage, undefined, null, undefined, collOpts);
|
|
860
|
+
if (bannerImage.value !== null)
|
|
861
|
+
provenance.push(bannerImage.prov);
|
|
862
|
+
if (featuredImage.value !== null)
|
|
863
|
+
provenance.push(featuredImage.prov);
|
|
628
864
|
// the collection-scope `artifacts` manifest — content-bearing collection fields (banner,
|
|
629
865
|
// featured image, plus any non-reserved field). The representative-token image is a projection
|
|
630
866
|
// courtesy, not a collection artifact; a collection-scope `image` doubles as the per-token
|
|
@@ -641,7 +877,7 @@ export async function buildContractMetadata(client, state, baseUrl, chainId, dis
|
|
|
641
877
|
key: n,
|
|
642
878
|
// collection surface has no token — a field renderer gets the sentinel id (mirrors on-chain)
|
|
643
879
|
mimeType: await fieldMimeType(client, state, entry, n, COLLECTION_TOKEN_ID, display, storage),
|
|
644
|
-
uri: fieldArtifactUri(entry, `${baseUrl}/c/${chainId}/${state.address}/data/${n}`, '0', display),
|
|
880
|
+
uri: fieldArtifactUri(entry, `${baseUrl}/c/${chainId}/${state.address}/data/${n}`, '0', display, collOpts.gateways),
|
|
645
881
|
});
|
|
646
882
|
if (!RESERVED_FIELDS.has(n))
|
|
647
883
|
provenance.push(artifactFieldProv(n, entry, false));
|
|
@@ -656,16 +892,18 @@ export async function buildContractMetadata(client, state, baseUrl, chainId, dis
|
|
|
656
892
|
json.description = description.value;
|
|
657
893
|
if (externalLink.value !== null)
|
|
658
894
|
json.external_link = externalLink.value;
|
|
659
|
-
if (
|
|
660
|
-
json.
|
|
895
|
+
if (creator.value !== null)
|
|
896
|
+
json.creator = creator.value;
|
|
661
897
|
if (displayNotes.value !== null)
|
|
662
898
|
json.display_notes = displayNotes.value;
|
|
663
|
-
if (
|
|
664
|
-
json.
|
|
899
|
+
if (creatorLinks.value !== null)
|
|
900
|
+
json.creator_links = creatorLinks.value;
|
|
665
901
|
if (license.value !== null)
|
|
666
902
|
json.license = license.value;
|
|
667
|
-
if (bannerImage)
|
|
668
|
-
json.banner_image = bannerImage;
|
|
903
|
+
if (bannerImage.value !== null)
|
|
904
|
+
json.banner_image = bannerImage.value;
|
|
905
|
+
if (featuredImage.value !== null)
|
|
906
|
+
json.featured_image = featuredImage.value;
|
|
669
907
|
if (artifacts.length)
|
|
670
908
|
json.artifacts = artifacts;
|
|
671
909
|
return json;
|