@artblocks/abx-token-api 0.1.0-alpha.2 → 0.1.0-alpha.20
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/dist/code.d.ts +33 -12
- package/dist/code.d.ts.map +1 -1
- package/dist/code.js +45 -55
- 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 +85 -0
- package/dist/control-plane.d.ts.map +1 -0
- package/dist/control-plane.js +721 -0
- package/dist/control-plane.js.map +1 -0
- package/dist/dashboard.d.ts +1 -1
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +39 -16
- 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 +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -10
- package/dist/index.js.map +1 -1
- package/dist/metadata.d.ts +29 -2
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +298 -104
- 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 -27
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +251 -418
- package/dist/server.js.map +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 +4 -4
- 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,134 @@ 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
|
+
// (Found by abx-services reading this function, not by a test: nothing observable differed.)
|
|
165
|
+
return { value, prov: onChainProv(field, R.renderer, fromCollection) };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
// `IAbxFieldRenderer`'s first invariant is NEVER REVERT, but a resolver cannot rely on a
|
|
170
|
+
// third-party contract honouring it — a reverting renderer must degrade this one field, not
|
|
171
|
+
// 500 the whole token's metadata. We still fall through to the operator's off-chain value
|
|
172
|
+
// (better than nothing), but the provenance says the on-chain attempt failed rather than
|
|
173
|
+
// quietly reporting `off-chain` as if nothing were committed on-chain.
|
|
174
|
+
rendererFailure = err.message;
|
|
175
|
+
console.warn(`[metadata] field renderer for '${field}' on ${tokenAddress} reverted: ${rendererFailure}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const noteRendererFailure = (out) => rendererFailure === null
|
|
179
|
+
? out
|
|
180
|
+
: { ...out, prov: { ...out.prov, note: `${out.prov.note} — on-chain field renderer reverted, so it could not be used` } };
|
|
181
|
+
// Content-addressed locator (`ipfs`/`arweave`): the CID/txid is identity, the collection's
|
|
182
|
+
// preferred gateway is what makes it dereferenceable. Mirrors {AbxMetadataRenderer._appendText}'s
|
|
183
|
+
// v11 arm. A value that locates nothing (`ipfs://` with no CID) falls through and omits, rather
|
|
184
|
+
// than emitting a bare gateway prefix that 404s.
|
|
185
|
+
if (entry && (entry.representation === R.ipfs || entry.representation === R.arweave)) {
|
|
186
|
+
const wrapped = gatewayFieldUrl(entry, opts.gateways, opts.tokenId === COLLECTION_TOKEN_ID ? undefined : opts.tokenId);
|
|
187
|
+
if (wrapped !== null) {
|
|
188
|
+
return {
|
|
189
|
+
value: wrapped,
|
|
190
|
+
prov: {
|
|
191
|
+
field,
|
|
192
|
+
source: entry.representation,
|
|
193
|
+
status: 'on-chain',
|
|
194
|
+
note: scopeNote(GATEWAY_SOURCE_NOTE, fromCollection),
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
81
198
|
}
|
|
82
199
|
// on-chain locator carried AS the text value (url, or url-template with {id} substituted).
|
|
83
200
|
if (entry && (entry.representation === R.url || entry.representation === R.urlTemplate)) {
|
|
@@ -88,19 +205,48 @@ async function resolveText(client, fields, field, offChain, fallback, fallbackPr
|
|
|
88
205
|
prov: {
|
|
89
206
|
field,
|
|
90
207
|
source: entry.representation,
|
|
91
|
-
onChain: true,
|
|
92
208
|
status: 'on-chain',
|
|
93
|
-
note: scopeNote(
|
|
209
|
+
note: scopeNote('stored on chain; the value is a locator', fromCollection),
|
|
94
210
|
},
|
|
95
211
|
};
|
|
96
212
|
}
|
|
97
213
|
if (offChain != null) {
|
|
98
|
-
return { value: offChain, prov: offChainProv(field, entry, offChain) };
|
|
214
|
+
return noteRendererFailure({ value: offChain, prov: offChainProv(field, entry, offChain) });
|
|
99
215
|
}
|
|
100
216
|
if (fallback != null) {
|
|
101
|
-
return {
|
|
217
|
+
return noteRendererFailure({
|
|
218
|
+
value: fallback,
|
|
219
|
+
prov: fallbackProv ?? placeholderProv(field, 'default (no on-chain or off-chain value)'),
|
|
220
|
+
});
|
|
102
221
|
}
|
|
103
|
-
return { value: null, prov: placeholderProv(field) };
|
|
222
|
+
return noteRendererFailure({ value: null, prov: placeholderProv(field) });
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The project's preferred gateway prefix per scheme, resolved ONCE per document.
|
|
226
|
+
*
|
|
227
|
+
* The off-chain twin of `AbxMetadataRenderer._gatewayPrefix` (spec v11): the collection's reserved
|
|
228
|
+
* `abx_gateway_ipfs` / `abx_gateway_arweave` field, else this host's env default, else the public
|
|
229
|
+
* floor. Two conforming resolvers given the same chain must emit the same `image`, so the env read
|
|
230
|
+
* is a FLOOR and never an override — a project that stated a preference on chain gets that
|
|
231
|
+
* preference from every resolver, and a host's own gateway only ever fills a silence (which is the
|
|
232
|
+
* only way a managed provider serving a token it does not own can offer a better default).
|
|
233
|
+
*/
|
|
234
|
+
export function projectGateways(state) {
|
|
235
|
+
const env = gatewayConfigFromEnv();
|
|
236
|
+
return {
|
|
237
|
+
ipfs: projectGatewayPrefix(state, 'ipfs', env),
|
|
238
|
+
arweave: projectGatewayPrefix(state, 'arweave', env),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** The provenance note for a content-addressed locator. Byte-identical to the deployed renderer's
|
|
242
|
+
* `_sourceNote` for `ipfs`/`arweave` — deliberately "served through", never "verified": the
|
|
243
|
+
* locator is on chain, the bytes are not, and nothing here re-hashes what a gateway returns. */
|
|
244
|
+
const GATEWAY_SOURCE_NOTE = "stored on chain; a content-addressed locator, served through the collection's preferred gateway";
|
|
245
|
+
/** Project a content-addressed field value the way the deployed renderer does, or `null` when the
|
|
246
|
+
* value locates nothing. `tokenId` omitted ⇒ no `{id}` substitution (the collection surface). */
|
|
247
|
+
function gatewayFieldUrl(entry, gateways, tokenId) {
|
|
248
|
+
const network = entry.representation === R.ipfs ? 'ipfs' : 'arweave';
|
|
249
|
+
return projectGatewayUrl(network, inlineText(entry).trim(), gateways[network], tokenId);
|
|
104
250
|
}
|
|
105
251
|
/**
|
|
106
252
|
* Map a locator to the provenance `source` describing where the bytes actually live. Locators are
|
|
@@ -125,7 +271,10 @@ function sourceForLocator(locator) {
|
|
|
125
271
|
* one (bridged or from the active backend) so the heavy asset resolves peer-to-peer; else
|
|
126
272
|
* this node's `/…/image` route serves it from custody. Either way it's `anchored` by the
|
|
127
273
|
* on-chain hash (verify via `/verify`).
|
|
128
|
-
* -
|
|
274
|
+
* - content-addressed on-chain (`ipfs`/`arweave`) → the CID/txid wrapped in the collection's
|
|
275
|
+
* preferred gateway prefix, so what lands in `image` is an `https://` a wallet can render;
|
|
276
|
+
* `on-chain`.
|
|
277
|
+
* - locator on-chain (`url`) → the on-chain pointer IS the URL; `on-chain`.
|
|
129
278
|
* - unset → this node's `/…/image` route serves the deterministic generative placeholder.
|
|
130
279
|
*/
|
|
131
280
|
async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
@@ -151,7 +300,6 @@ async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
|
151
300
|
prov: {
|
|
152
301
|
field: F.image,
|
|
153
302
|
source: 'url-template',
|
|
154
|
-
onChain: true,
|
|
155
303
|
status: 'on-chain',
|
|
156
304
|
note: scopeNote('on-chain url template -> off-chain content', fromCollection),
|
|
157
305
|
},
|
|
@@ -168,7 +316,6 @@ async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
|
168
316
|
prov: {
|
|
169
317
|
field: F.image,
|
|
170
318
|
source: sourceForLocator(locator),
|
|
171
|
-
onChain: false,
|
|
172
319
|
status: 'anchored',
|
|
173
320
|
anchor,
|
|
174
321
|
note: `off-chain bytes (${sourceForLocator(locator)}); on-chain ${anchor} integrity anchor`,
|
|
@@ -181,7 +328,6 @@ async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
|
181
328
|
prov: {
|
|
182
329
|
field: F.image,
|
|
183
330
|
source: anchor,
|
|
184
|
-
onChain: false,
|
|
185
331
|
status: 'anchored',
|
|
186
332
|
anchor,
|
|
187
333
|
note: `off-chain bytes (served by this node); on-chain ${anchor} integrity anchor`,
|
|
@@ -189,27 +335,41 @@ async function resolveImage(state, token, baseUrl, chainId, display, storage) {
|
|
|
189
335
|
};
|
|
190
336
|
}
|
|
191
337
|
// 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
|
|
338
|
+
// at this node's route, which eth_calls the renderer and serves the computed bytes (302-ing to the
|
|
339
|
+
// locator if the computed content turns out to BE one). The provenance is the shared one: the route
|
|
340
|
+
// the bytes took is identical to any other on-chain content, and only the served `image` VALUE is
|
|
341
|
+
// this field's documented exception. A hand-built note here drifted from the deployed renderer's
|
|
342
|
+
// `_sourceNote` wording for the same representation.
|
|
193
343
|
if (entry.representation === R.renderer) {
|
|
194
|
-
return {
|
|
195
|
-
url: nodeUrl,
|
|
196
|
-
prov: {
|
|
197
|
-
field: F.image,
|
|
198
|
-
source: 'renderer',
|
|
199
|
-
onChain: true,
|
|
200
|
-
status: 'on-chain',
|
|
201
|
-
note: scopeNote('on-chain field renderer (computed from chain state at read)', fromCollection),
|
|
202
|
-
},
|
|
203
|
-
};
|
|
344
|
+
return { url: nodeUrl, prov: onChainProv(F.image, R.renderer, fromCollection) };
|
|
204
345
|
}
|
|
205
|
-
//
|
|
346
|
+
// Content-addressed ON-CHAIN (ipfs/arweave): the value is identity, not a URL. Wrap it in the
|
|
347
|
+
// collection's preferred gateway — the twin of {AbxMetadataRenderer._resolveImage}'s v11 arm.
|
|
348
|
+
// Before v11 both planes emitted the raw `ipfs://…` here, which no marketplace dereferences, and
|
|
349
|
+
// `--onchain-uri --backend ipfs` worked around it by baking a gateway host into a `url` field.
|
|
350
|
+
if (entry.representation === R.ipfs || entry.representation === R.arweave) {
|
|
351
|
+
const wrapped = gatewayFieldUrl(entry, projectGateways(state), token.tokenId);
|
|
352
|
+
if (wrapped !== null) {
|
|
353
|
+
return {
|
|
354
|
+
url: wrapped,
|
|
355
|
+
prov: {
|
|
356
|
+
field: F.image,
|
|
357
|
+
source: entry.representation,
|
|
358
|
+
status: 'on-chain',
|
|
359
|
+
note: scopeNote(GATEWAY_SOURCE_NOTE, fromCollection),
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// locates nothing → fall through to the node's placeholder route rather than a bare prefix
|
|
364
|
+
return { url: nodeUrl, prov: placeholderProv(F.image, 'placeholder — the committed locator names no content') };
|
|
365
|
+
}
|
|
366
|
+
// Locator committed ON-CHAIN (url): the on-chain value is itself the address.
|
|
206
367
|
const onChainLocator = inlineText(entry).trim();
|
|
207
368
|
return {
|
|
208
369
|
url: onChainLocator || nodeUrl,
|
|
209
370
|
prov: {
|
|
210
371
|
field: F.image,
|
|
211
372
|
source: entry.representation,
|
|
212
|
-
onChain: true,
|
|
213
373
|
status: 'on-chain',
|
|
214
374
|
note: scopeNote(`on-chain pointer (${entry.representation}) — bytes off-chain, located on-chain`, fromCollection),
|
|
215
375
|
},
|
|
@@ -250,7 +410,11 @@ export async function fieldMimeType(client, state, entry, field, tokenId, displa
|
|
|
250
410
|
return (await rendererContentType(client, state.address, entry, field, tokenId)) ?? 'application/octet-stream';
|
|
251
411
|
}
|
|
252
412
|
if (rep === R.inline || rep === R.inlineGzip || rep === R.reader || rep === R.readerGzip) {
|
|
253
|
-
|
|
413
|
+
// registry conventions for on-chain content bytes: an `image` is SVG, an `animation_url` is
|
|
414
|
+
// the HTML document the renderer wraps as `data:text/html` (spec v4). Anything else: the floor.
|
|
415
|
+
if (field === F.image)
|
|
416
|
+
return 'image/svg+xml';
|
|
417
|
+
return field === F.animationUrl ? 'text/html' : 'application/octet-stream';
|
|
254
418
|
}
|
|
255
419
|
if (rep === R.keccak256 || rep === R.sha256) {
|
|
256
420
|
const stored = storage ? await storage.get(entry.value).catch(() => null) : null;
|
|
@@ -260,15 +424,21 @@ export async function fieldMimeType(client, state, entry, field, tokenId, displa
|
|
|
260
424
|
return bridged ? contentTypeFromPath(bridged) : 'application/octet-stream';
|
|
261
425
|
}
|
|
262
426
|
const raw = inlineText(entry).trim();
|
|
427
|
+
// For ipfs/arweave the extension (if any) rides the stored value — no need to wrap first, and
|
|
428
|
+
// wrapping would only add a gateway host that never carries the extension.
|
|
263
429
|
return contentTypeFromPath(rep === R.urlTemplate ? applyTemplate(raw, tokenId) : raw);
|
|
264
430
|
}
|
|
265
431
|
/** A field artifact's `uri`: locator forms verbatim (content-addressed preferred, `{id}`
|
|
266
432
|
* substituted); custody-hash forms prefer the bridged durable locator; everything this node
|
|
267
433
|
* serves itself (inline/reader/renderer) rides the given `/data/{field}` route. Total — every
|
|
268
434
|
* representation resolves to some uri (the complete-listing rule). */
|
|
269
|
-
function fieldArtifactUri(entry, dataRoute, tokenId, display) {
|
|
435
|
+
function fieldArtifactUri(entry, dataRoute, tokenId, display, gateways) {
|
|
270
436
|
const rep = entry.representation;
|
|
271
|
-
|
|
437
|
+
// Content-addressed forms carry the same gateway wrap the projected fields get, so a manifest
|
|
438
|
+
// entry and the `image` it duplicates never point at two different URLs for the same bytes.
|
|
439
|
+
if (rep === R.ipfs || rep === R.arweave)
|
|
440
|
+
return gatewayFieldUrl(entry, gateways, tokenId) ?? dataRoute;
|
|
441
|
+
if (rep === R.url)
|
|
272
442
|
return inlineText(entry).trim() || dataRoute;
|
|
273
443
|
if (rep === R.urlTemplate)
|
|
274
444
|
return applyTemplate(inlineText(entry), tokenId);
|
|
@@ -283,21 +453,17 @@ function artifactFieldProv(field, entry, fromCollection) {
|
|
|
283
453
|
if (rep === R.inline || rep === R.inlineGzip || rep === R.reader || rep === R.readerGzip) {
|
|
284
454
|
return onChainProv(field, rep, fromCollection);
|
|
285
455
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
note: scopeNote('on-chain field renderer (computed from chain state at read)', fromCollection),
|
|
293
|
-
};
|
|
294
|
-
}
|
|
456
|
+
// Same call as the branch above — `renderer` is on-chain content like `inline`/`reader`, and the
|
|
457
|
+
// note belongs to `sourceNote` (which mirrors the deployed renderer's `_sourceNote` verbatim). This
|
|
458
|
+
// branch used to hand-build the object with a longer note of its own, so one function delegated in
|
|
459
|
+
// one arm and duplicated in the next.
|
|
460
|
+
if (rep === R.renderer)
|
|
461
|
+
return onChainProv(field, rep, fromCollection);
|
|
295
462
|
if (rep === R.keccak256 || rep === R.sha256) {
|
|
296
463
|
const anchor = rep;
|
|
297
464
|
return {
|
|
298
465
|
field,
|
|
299
466
|
source: anchor,
|
|
300
|
-
onChain: false,
|
|
301
467
|
status: 'anchored',
|
|
302
468
|
anchor,
|
|
303
469
|
note: `off-chain bytes; on-chain ${anchor} integrity anchor`,
|
|
@@ -306,7 +472,6 @@ function artifactFieldProv(field, entry, fromCollection) {
|
|
|
306
472
|
return {
|
|
307
473
|
field,
|
|
308
474
|
source: rep,
|
|
309
|
-
onChain: true,
|
|
310
475
|
status: 'on-chain',
|
|
311
476
|
note: scopeNote(`on-chain pointer (${rep}) — bytes off-chain, located on-chain`, fromCollection),
|
|
312
477
|
};
|
|
@@ -352,7 +517,7 @@ async function buildTokenArtifacts(client, state, token, baseUrl, chainId, displ
|
|
|
352
517
|
entries.push({
|
|
353
518
|
key: name,
|
|
354
519
|
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),
|
|
520
|
+
uri: fieldArtifactUri(entry, `${baseUrl}/t/${chainId}/${state.address}/${token.tokenId}/data/${name}`, token.tokenId, display, projectGateways(state)),
|
|
356
521
|
});
|
|
357
522
|
prov.push(artifactFieldProv(name, entry, fromCollection));
|
|
358
523
|
}
|
|
@@ -375,7 +540,6 @@ async function buildTokenArtifacts(client, state, token, baseUrl, chainId, displ
|
|
|
375
540
|
prov.push({
|
|
376
541
|
field: key,
|
|
377
542
|
source: `effect:${row.effectKey}`,
|
|
378
|
-
onChain: false,
|
|
379
543
|
status: 'off-chain',
|
|
380
544
|
note: 'derived effect output at the current inputsHash (re-creatable bytes)',
|
|
381
545
|
});
|
|
@@ -408,7 +572,6 @@ function resolveAttributes(fields, offChain, collectionFields, fromRender = fals
|
|
|
408
572
|
prov = {
|
|
409
573
|
field: F.attributes,
|
|
410
574
|
source: 'inline',
|
|
411
|
-
onChain: true,
|
|
412
575
|
status: 'on-chain',
|
|
413
576
|
note: fromRender
|
|
414
577
|
? 'on-chain inline JSON + render-effect traits (+ operator), stitched (on-chain wins per trait_type)'
|
|
@@ -425,7 +588,6 @@ function resolveAttributes(fields, offChain, collectionFields, fromRender = fals
|
|
|
425
588
|
prov = {
|
|
426
589
|
field: F.attributes,
|
|
427
590
|
source: fromRender ? 'effect:render' : 'off-chain',
|
|
428
|
-
onChain: false,
|
|
429
591
|
status: 'off-chain',
|
|
430
592
|
note: fromRender
|
|
431
593
|
? 'script-reported traits captured at render (+ operator traits; render wins per trait_type)'
|
|
@@ -434,7 +596,8 @@ function resolveAttributes(fields, offChain, collectionFields, fromRender = fals
|
|
|
434
596
|
}
|
|
435
597
|
return { attributes: merged, prov };
|
|
436
598
|
}
|
|
437
|
-
/**
|
|
599
|
+
/** Token metadata JSON — the `tokenURI` target (ERC-721) / the `uri(id)` target (ERC-1155
|
|
600
|
+
* editions), assembled identically either way + `abx_provenance` + `artifacts`. */
|
|
438
601
|
export async function buildTokenMetadata(client, state, token, baseUrl, chainId, display = {}, storage,
|
|
439
602
|
// The effect-artifact registry read surface: rows a producer PUBLISHED to this resolver (or
|
|
440
603
|
// recorded co-located). Feeds the `artifacts` manifest and the image seam's locator check —
|
|
@@ -463,7 +626,6 @@ plane) {
|
|
|
463
626
|
image.prov = {
|
|
464
627
|
field: F.image,
|
|
465
628
|
source: 'effect:render',
|
|
466
|
-
onChain: false,
|
|
467
629
|
status: 'off-chain',
|
|
468
630
|
note: viaLocator
|
|
469
631
|
? 'render effect output at the current inputsHash, published as a durable locator (resolver 302-redirects)'
|
|
@@ -484,12 +646,12 @@ plane) {
|
|
|
484
646
|
// on-chain renderer's `_field`), and url-template fields substitute this token's id.
|
|
485
647
|
const cf = state.collectionFields;
|
|
486
648
|
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);
|
|
649
|
+
const opts = { collectionFields: cf, tokenId: tid, gateways: projectGateways(state) };
|
|
650
|
+
const name = await resolveText(client, state.address, f, F.name, undefined, `${state.name ?? state.address} #${token.tokenId}`, nameFallbackProv(state.name != null, ' + #id'), opts);
|
|
651
|
+
const description = await resolveText(client, state.address, f, F.description, display.description, null, undefined, opts); // optional → omit if unset
|
|
652
|
+
const externalUrl = await resolveText(client, state.address, f, F.externalUrl, display.externalUrl, null, undefined, opts); // optional → omit if unset
|
|
653
|
+
const backgroundColor = await resolveText(client, state.address, f, F.backgroundColor, undefined, null, undefined, opts);
|
|
654
|
+
const youtubeUrl = await resolveText(client, state.address, f, F.youtubeUrl, undefined, null, undefined, opts);
|
|
493
655
|
provenance.push(name.prov); // name is required — always present
|
|
494
656
|
// image: required — always served via our URL; image_data carries inline SVG if on-chain SVG.
|
|
495
657
|
const img = fieldOf(f, F.image);
|
|
@@ -501,8 +663,13 @@ plane) {
|
|
|
501
663
|
if (externalUrl.value !== null)
|
|
502
664
|
provenance.push(externalUrl.prov);
|
|
503
665
|
// 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
|
-
|
|
666
|
+
// live-view route (suppressible via the `display.animation = none` contract param). On-chain
|
|
667
|
+
// content is served at this node's data route rather than inlined (see `uriRoute`); locator forms
|
|
668
|
+
// pass through untouched. The on-chain renderer still inlines it — it has no route to offer.
|
|
669
|
+
const animation = await resolveText(client, state.address, f, F.animationUrl, undefined, null, undefined, {
|
|
670
|
+
...opts,
|
|
671
|
+
uriRoute: `${baseUrl}/t/${chainId}/${state.address}/${token.tokenId}/data/${F.animationUrl}`,
|
|
672
|
+
});
|
|
506
673
|
let animationValue = animation.value;
|
|
507
674
|
let animationProv = animation.prov;
|
|
508
675
|
if (animationValue === null && liveViewEnabled(state)) {
|
|
@@ -510,7 +677,6 @@ plane) {
|
|
|
510
677
|
animationProv = {
|
|
511
678
|
field: F.animationUrl,
|
|
512
679
|
source: 'live-view',
|
|
513
|
-
onChain: false,
|
|
514
680
|
status: 'off-chain',
|
|
515
681
|
note: 'live view derived from the on-chain code (canonical tokenData injected at load)',
|
|
516
682
|
};
|
|
@@ -522,22 +688,27 @@ plane) {
|
|
|
522
688
|
// traits (captured at the current inputsHash), which win over the operator's off-chain
|
|
523
689
|
// traits. ABX facts (version, canonical, royalty) are NOT traits; they live in this
|
|
524
690
|
// provenance block / ERC-2981, never in the marketplace trait array.
|
|
691
|
+
// `traits` is a BOUND output (`effects.md → Bound vs referenced`): its content is held by this
|
|
692
|
+
// node, so there are exactly two places it can be, and BOTH are hash-gated by the key itself —
|
|
693
|
+
// a param change re-addresses the key, so stale traits go unstitched rather than being attributed
|
|
694
|
+
// to a state they don't depict.
|
|
695
|
+
// - the registry row (a producer REGISTERED it over /v1/effect-artifacts: bytes ride the row),
|
|
696
|
+
// - this node's storage (a CO-LOCATED runner wrote the artifact to the shared backend).
|
|
525
697
|
let renderTraits = null;
|
|
526
|
-
if (isCodeProject(state) && storage) {
|
|
698
|
+
if (isCodeProject(state) && (plane || storage)) {
|
|
527
699
|
try {
|
|
528
|
-
const { key, found } = await currentRenderArtifact(client, state, token, storage, 'traits', {
|
|
700
|
+
const { key, found } = await currentRenderArtifact(client, state, token, storage ?? undefined, 'traits', {
|
|
529
701
|
hash: settledHash ?? undefined,
|
|
530
702
|
});
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
703
|
+
const boundBytes = plane?.get(key)?.bytes ?? null;
|
|
704
|
+
const raw = boundBytes ?? (found ? (await storage?.get(key))?.bytes ?? null : null);
|
|
705
|
+
const parsed = raw ? JSON.parse(new TextDecoder().decode(raw)) : null;
|
|
706
|
+
// scripts report either the OpenSea array or the natural object form
|
|
707
|
+
// (`abx.traits({Palette: 'Dusk'})`) — normalize the latter.
|
|
708
|
+
if (Array.isArray(parsed))
|
|
709
|
+
renderTraits = parsed;
|
|
710
|
+
else if (parsed && typeof parsed === 'object') {
|
|
711
|
+
renderTraits = Object.entries(parsed).map(([trait_type, value]) => ({ trait_type, value: value }));
|
|
541
712
|
}
|
|
542
713
|
}
|
|
543
714
|
catch {
|
|
@@ -555,6 +726,13 @@ plane) {
|
|
|
555
726
|
provenance.push(backgroundColor.prov);
|
|
556
727
|
if (youtubeUrl.value)
|
|
557
728
|
provenance.push(youtubeUrl.prov);
|
|
729
|
+
// NO `abx_params` block. It was emitted here and by the on-chain renderer and read back by
|
|
730
|
+
// nobody — the only consumers in the repo were its own tests. Params already enumerate directly
|
|
731
|
+
// from the contract (`tokenParamKeys` / `tokenParam` / `paramSchemaKeys`), which is canonical and
|
|
732
|
+
// needs no indexer, and a code project's script gets them through `tokenData`. So the projection
|
|
733
|
+
// was a third serialization of data available two better ways: uncapped in size, quadratic to
|
|
734
|
+
// build, and duplicated across two implementations — which is precisely the sibling drift that
|
|
735
|
+
// produced the computed-image-locator bug. Traits that belong to marketplaces go in `attributes`.
|
|
558
736
|
// the `artifacts` manifest — the plane's complete listing (data-plane.md). Omitted when empty.
|
|
559
737
|
const artifacts = await buildTokenArtifacts(client, state, token, baseUrl, chainId, display, storage, plane, {
|
|
560
738
|
imageUrl: image.url,
|
|
@@ -592,20 +770,24 @@ export async function buildContractMetadata(client, state, baseUrl, chainId, dis
|
|
|
592
770
|
const f = state.collectionFields;
|
|
593
771
|
const provenance = [];
|
|
594
772
|
// Representative image: the first token that actually carries an `image` field, else the
|
|
595
|
-
// first token — so "first with an image" surfaces a real
|
|
773
|
+
// first token — so "first with an image" surfaces a real work rather than a placeholder.
|
|
596
774
|
const rep = state.tokens.find((t) => fieldOf(t.fields, F.image)) ?? state.tokens[0];
|
|
597
775
|
const image = rep ? await resolveImage(state, rep, baseUrl, chainId, display, storage) : undefined;
|
|
598
|
-
//
|
|
776
|
+
// The collection surface has no token, so a `renderer`-represented field is called with the
|
|
777
|
+
// sentinel id — the same one {AbxMetadataRenderer} passes for its collection reads, so a field
|
|
778
|
+
// renderer that serves both scopes sees an identical call from chain and from here.
|
|
779
|
+
const collOpts = { tokenId: COLLECTION_TOKEN_ID, gateways: projectGateways(state) };
|
|
780
|
+
// name is required (fallback = the on-chain collection name); description + external_link
|
|
599
781
|
// 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);
|
|
782
|
+
const name = await resolveText(client, state.address, f, F.name, undefined, state.name ?? state.address, nameFallbackProv(state.name != null, ''), collOpts);
|
|
783
|
+
const description = await resolveText(client, state.address, f, F.description, display.description, null, undefined, collOpts);
|
|
784
|
+
const externalLink = await resolveText(client, state.address, f, F.externalLink, display.externalUrl, null, undefined, collOpts);
|
|
603
785
|
// authorship + rights (collection scope) — reserved fields, on-chain-only (no operator source):
|
|
604
786
|
// 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);
|
|
787
|
+
const creator = await resolveText(client, state.address, f, F.creator, undefined, null, undefined, collOpts);
|
|
788
|
+
const displayNotes = await resolveText(client, state.address, f, F.displayNotes, undefined, null, undefined, collOpts);
|
|
789
|
+
const creatorLinks = await resolveText(client, state.address, f, F.creatorLinks, undefined, null, undefined, collOpts);
|
|
790
|
+
const license = await resolveText(client, state.address, f, F.license, undefined, null, undefined, collOpts);
|
|
609
791
|
provenance.push(name.prov);
|
|
610
792
|
if (image)
|
|
611
793
|
provenance.push(image.prov);
|
|
@@ -613,18 +795,28 @@ export async function buildContractMetadata(client, state, baseUrl, chainId, dis
|
|
|
613
795
|
provenance.push(description.prov);
|
|
614
796
|
if (externalLink.value !== null)
|
|
615
797
|
provenance.push(externalLink.prov);
|
|
616
|
-
if (
|
|
617
|
-
provenance.push(
|
|
798
|
+
if (creator.value !== null)
|
|
799
|
+
provenance.push(creator.prov);
|
|
618
800
|
if (displayNotes.value !== null)
|
|
619
801
|
provenance.push(displayNotes.prov);
|
|
620
|
-
if (
|
|
621
|
-
provenance.push(
|
|
802
|
+
if (creatorLinks.value !== null)
|
|
803
|
+
provenance.push(creatorLinks.prov);
|
|
622
804
|
if (license.value !== null)
|
|
623
805
|
provenance.push(license.prov);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
806
|
+
// The two reserved collection IMAGE keys. Both are top-level here, and both are top-level on the
|
|
807
|
+
// on-chain renderer as of spec v10 — the point of the three-list table in
|
|
808
|
+
// specs/protocol/onchain-metadata.md is that a key BOTH planes can emit must not be emitted by only
|
|
809
|
+
// one of them. Two things were wrong before v10: `featured_image` appeared in `artifacts` alone
|
|
810
|
+
// (reserved, so filtered out of the top level, yet still listed — "reserved but only in artifacts",
|
|
811
|
+
// which is neither), and `banner_image` was top-level only for `representation === url`, so an
|
|
812
|
+
// `inline` or `reader` banner silently dropped even though both resolve fine. Every chain-reachable
|
|
813
|
+
// representation now counts, matching `_appendContractText` on the renderer.
|
|
814
|
+
const bannerImage = await resolveText(client, state.address, f, F.bannerImage, undefined, null, undefined, collOpts);
|
|
815
|
+
const featuredImage = await resolveText(client, state.address, f, F.featuredImage, undefined, null, undefined, collOpts);
|
|
816
|
+
if (bannerImage.value !== null)
|
|
817
|
+
provenance.push(bannerImage.prov);
|
|
818
|
+
if (featuredImage.value !== null)
|
|
819
|
+
provenance.push(featuredImage.prov);
|
|
628
820
|
// the collection-scope `artifacts` manifest — content-bearing collection fields (banner,
|
|
629
821
|
// featured image, plus any non-reserved field). The representative-token image is a projection
|
|
630
822
|
// courtesy, not a collection artifact; a collection-scope `image` doubles as the per-token
|
|
@@ -641,7 +833,7 @@ export async function buildContractMetadata(client, state, baseUrl, chainId, dis
|
|
|
641
833
|
key: n,
|
|
642
834
|
// collection surface has no token — a field renderer gets the sentinel id (mirrors on-chain)
|
|
643
835
|
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),
|
|
836
|
+
uri: fieldArtifactUri(entry, `${baseUrl}/c/${chainId}/${state.address}/data/${n}`, '0', display, collOpts.gateways),
|
|
645
837
|
});
|
|
646
838
|
if (!RESERVED_FIELDS.has(n))
|
|
647
839
|
provenance.push(artifactFieldProv(n, entry, false));
|
|
@@ -656,16 +848,18 @@ export async function buildContractMetadata(client, state, baseUrl, chainId, dis
|
|
|
656
848
|
json.description = description.value;
|
|
657
849
|
if (externalLink.value !== null)
|
|
658
850
|
json.external_link = externalLink.value;
|
|
659
|
-
if (
|
|
660
|
-
json.
|
|
851
|
+
if (creator.value !== null)
|
|
852
|
+
json.creator = creator.value;
|
|
661
853
|
if (displayNotes.value !== null)
|
|
662
854
|
json.display_notes = displayNotes.value;
|
|
663
|
-
if (
|
|
664
|
-
json.
|
|
855
|
+
if (creatorLinks.value !== null)
|
|
856
|
+
json.creator_links = creatorLinks.value;
|
|
665
857
|
if (license.value !== null)
|
|
666
858
|
json.license = license.value;
|
|
667
|
-
if (bannerImage)
|
|
668
|
-
json.banner_image = bannerImage;
|
|
859
|
+
if (bannerImage.value !== null)
|
|
860
|
+
json.banner_image = bannerImage.value;
|
|
861
|
+
if (featuredImage.value !== null)
|
|
862
|
+
json.featured_image = featuredImage.value;
|
|
669
863
|
if (artifacts.length)
|
|
670
864
|
json.artifacts = artifacts;
|
|
671
865
|
return json;
|