@feralfile/cli 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.json.example +5 -2
- package/dist/package.json +5 -2
- package/dist/src/commands/device.js +66 -7
- package/dist/src/commands/find.js +165 -98
- package/dist/src/commands/helpers/device-discovery.js +23 -3
- package/dist/src/commands/helpers/device-removal.js +40 -0
- package/dist/src/commands/play.js +3 -3
- package/dist/src/commands/sign.js +1 -1
- package/dist/src/config.js +55 -1
- package/dist/src/utilities/device-lookup.js +3 -2
- package/dist/src/utilities/device-upsert.js +19 -5
- package/dist/src/utilities/ff1-device.js +127 -25
- package/dist/src/utilities/ff1-relayer.js +105 -0
- package/dist/src/utilities/functions.js +2 -2
- package/dist/src/utilities/handoff-client.js +135 -0
- package/dist/src/utilities/handoff-crypto.js +132 -0
- package/dist/src/utilities/index.js +1 -1
- package/dist/src/utilities/playlist-builder.js +2 -2
- package/dist/src/utilities/ssh-access.js +1 -5
- package/dist/src/utilities/topic-store.js +33 -0
- package/docs/CONFIGURATION.md +31 -2
- package/docs/PROJECT_SPEC.md +6 -0
- package/docs/README.md +4 -3
- package/package.json +5 -2
- package/dist/src/utilities/marketplace-url.js +0 -443
|
@@ -1,443 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Marketplace URL parser for `ff-cli find`.
|
|
4
|
-
*
|
|
5
|
-
* Accepts user input in any of these forms and returns a normalized result:
|
|
6
|
-
* - Marketplace token URL (Objkt, fxhash, Art Blocks, OpenSea, SuperRare, Feral File, Neort, Verse)
|
|
7
|
-
* - Raw on-chain coordinates: "ethereum:0xabc...:123" / "tezos:KT1...:456"
|
|
8
|
-
* - Wallet address: 0x{40-hex} (Ethereum) or tz1/tz2/tz3{33} (Tezos)
|
|
9
|
-
*
|
|
10
|
-
* The unified entry point is {@link parseFindInput}. Per-marketplace parsers
|
|
11
|
-
* are exposed for unit testing.
|
|
12
|
-
*
|
|
13
|
-
* Series URLs (fxhash `/generative/...`, Feral File `/artworks/...`,
|
|
14
|
-
* Art Blocks `/projects/...`) are recognized but not resolved in v1 — they
|
|
15
|
-
* require a per-marketplace API call to derive a representative token. The
|
|
16
|
-
* parser returns an `unsupported` result with a clear reason so the caller
|
|
17
|
-
* can surface guidance instead of a generic "could not parse" error.
|
|
18
|
-
*/
|
|
19
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.parseFindInput = parseFindInput;
|
|
21
|
-
exports.parseMarketplaceUrl = parseMarketplaceUrl;
|
|
22
|
-
exports.parseObjkt = parseObjkt;
|
|
23
|
-
exports.parseArtBlocks = parseArtBlocks;
|
|
24
|
-
exports.parseFxhash = parseFxhash;
|
|
25
|
-
exports.parseOpenSea = parseOpenSea;
|
|
26
|
-
exports.parseSuperRare = parseSuperRare;
|
|
27
|
-
exports.parseNeort = parseNeort;
|
|
28
|
-
exports.parseVerse = parseVerse;
|
|
29
|
-
exports.parseRaster = parseRaster;
|
|
30
|
-
exports.parseFeralFile = parseFeralFile;
|
|
31
|
-
const ETH_ADDR = /^0x[a-fA-F0-9]{40}$/;
|
|
32
|
-
const TEZOS_ADDR = /^(tz1|tz2|tz3)[1-9A-HJ-NP-Za-km-z]{33}$/;
|
|
33
|
-
const RAW_COORDS = /^(ethereum|tezos):([^:]+):([^:]+)$/i;
|
|
34
|
-
/**
|
|
35
|
-
* Parse any user-supplied input for the `ff-cli find` command.
|
|
36
|
-
*
|
|
37
|
-
* Returns:
|
|
38
|
-
* - `{ kind: 'token', ... }` — successfully extracted on-chain coordinates
|
|
39
|
-
* - `{ kind: 'address', ... }` — wallet/artist address (for the artist path)
|
|
40
|
-
* - `{ kind: 'unsupported', reason }` — recognized but cannot extract a token
|
|
41
|
-
* - `null` — input does not match any recognized form; caller may show help
|
|
42
|
-
*/
|
|
43
|
-
function parseFindInput(input) {
|
|
44
|
-
const trimmed = input.trim();
|
|
45
|
-
if (!trimmed) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
if (ETH_ADDR.test(trimmed)) {
|
|
49
|
-
return { kind: 'address', chain: 'ethereum', address: trimmed.toLowerCase() };
|
|
50
|
-
}
|
|
51
|
-
if (TEZOS_ADDR.test(trimmed)) {
|
|
52
|
-
return { kind: 'address', chain: 'tezos', address: trimmed };
|
|
53
|
-
}
|
|
54
|
-
const rawMatch = RAW_COORDS.exec(trimmed);
|
|
55
|
-
if (rawMatch) {
|
|
56
|
-
return {
|
|
57
|
-
kind: 'token',
|
|
58
|
-
source: 'raw',
|
|
59
|
-
coords: {
|
|
60
|
-
chain: rawMatch[1].toLowerCase(),
|
|
61
|
-
contract: normalizeContract(rawMatch[2]),
|
|
62
|
-
tokenId: rawMatch[3],
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
let parsedUrl;
|
|
67
|
-
try {
|
|
68
|
-
parsedUrl = new URL(trimmed);
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
return parseMarketplaceUrl(parsedUrl);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Dispatch a URL to the per-host parser. Returns `null` when the host is
|
|
77
|
-
* not one of the supported sources.
|
|
78
|
-
*/
|
|
79
|
-
function parseMarketplaceUrl(url) {
|
|
80
|
-
const host = url.hostname.toLowerCase().replace(/^www\./, '');
|
|
81
|
-
if (host === 'objkt.com' || host.endsWith('.objkt.com')) {
|
|
82
|
-
return parseObjkt(url);
|
|
83
|
-
}
|
|
84
|
-
if (host === 'artblocks.io' || host.endsWith('.artblocks.io')) {
|
|
85
|
-
return parseArtBlocks(url);
|
|
86
|
-
}
|
|
87
|
-
if (host === 'fxhash.xyz' || host.endsWith('.fxhash.xyz')) {
|
|
88
|
-
return parseFxhash(url);
|
|
89
|
-
}
|
|
90
|
-
if (host === 'feralfile.com' || host.endsWith('.feralfile.com')) {
|
|
91
|
-
return parseFeralFile(url);
|
|
92
|
-
}
|
|
93
|
-
if (host === 'opensea.io' || host.endsWith('.opensea.io')) {
|
|
94
|
-
return parseOpenSea(url);
|
|
95
|
-
}
|
|
96
|
-
if (host === 'superrare.com' || host.endsWith('.superrare.com')) {
|
|
97
|
-
return parseSuperRare(url);
|
|
98
|
-
}
|
|
99
|
-
if (host === 'neort.io' || host.endsWith('.neort.io')) {
|
|
100
|
-
return parseNeort(url);
|
|
101
|
-
}
|
|
102
|
-
if (host === 'verse.works' || host.endsWith('.verse.works')) {
|
|
103
|
-
return parseVerse(url);
|
|
104
|
-
}
|
|
105
|
-
if (host === 'raster.art' || host.endsWith('.raster.art')) {
|
|
106
|
-
return parseRaster(url);
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Lowercase Ethereum contracts; leave Tezos KT1 contracts case-sensitive
|
|
112
|
-
* (base58 prefix is case-significant on Tezos).
|
|
113
|
-
*/
|
|
114
|
-
function normalizeContract(contract) {
|
|
115
|
-
return contract.startsWith('0x') ? contract.toLowerCase() : contract;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Objkt URL forms:
|
|
119
|
-
* objkt.com/tokens/{KT1...}/{tokenId} — raw FA contract
|
|
120
|
-
* objkt.com/tokens/{alias}/{tokenId} — collection alias (e.g. "hicetnunc")
|
|
121
|
-
* objkt.com/asset/{KT1...}/{tokenId} — legacy redirect
|
|
122
|
-
*
|
|
123
|
-
* Aliases are resolved to KT1 contracts via
|
|
124
|
-
* {@link ./objkt-marketplace#resolveObjktAlias} (separate file because the
|
|
125
|
-
* lookup is async).
|
|
126
|
-
*/
|
|
127
|
-
function parseObjkt(url) {
|
|
128
|
-
const direct = /^\/(?:tokens|asset)\/(KT[A-Za-z0-9]+)\/(\d+)\/?$/.exec(url.pathname);
|
|
129
|
-
if (direct) {
|
|
130
|
-
return {
|
|
131
|
-
kind: 'token',
|
|
132
|
-
source: 'objkt',
|
|
133
|
-
coords: { chain: 'tezos', contract: direct[1], tokenId: direct[2] },
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
const alias = /^\/(?:tokens|asset)\/([a-zA-Z][a-zA-Z0-9_-]*)\/(\d+)\/?$/.exec(url.pathname);
|
|
137
|
-
if (alias) {
|
|
138
|
-
return { kind: 'objkt-alias', alias: alias[1], tokenId: alias[2] };
|
|
139
|
-
}
|
|
140
|
-
if (url.pathname.startsWith('/collections/') || url.pathname.startsWith('/collection/')) {
|
|
141
|
-
return {
|
|
142
|
-
kind: 'unsupported',
|
|
143
|
-
reason: 'Objkt collection URLs are not yet supported in v1. Paste a specific token URL ' +
|
|
144
|
-
'(objkt.com/tokens/{contract-or-alias}/{tokenId}) or use `tezos:{contract}:{tokenId}`.',
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
return {
|
|
148
|
-
kind: 'unsupported',
|
|
149
|
-
reason: `Objkt URL not recognized: ${url.pathname}. Expected /tokens/{contract-or-alias}/{tokenId}.`,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Art Blocks URL forms:
|
|
154
|
-
* artblocks.io/token/{0xcontract}-{tokenId} — direct token
|
|
155
|
-
* artblocks.io/marketplace/collections/{0xcontract}/tokens/{id} — marketplace UI token
|
|
156
|
-
* artblocks.io/collection/{slug} — collection (series)
|
|
157
|
-
* artblocks.io/collections/{slug} — plural; 307→singular
|
|
158
|
-
*
|
|
159
|
-
* Collection slugs are resolved to (contract, first_tokenId) via
|
|
160
|
-
* {@link ./ab-marketplace#resolveArtBlocksCollection} (one Hasura GraphQL
|
|
161
|
-
* call). `/projects/{id}` legacy URLs return 404 on artblocks.io and are
|
|
162
|
-
* surfaced as a clear unsupported message.
|
|
163
|
-
*/
|
|
164
|
-
function parseArtBlocks(url) {
|
|
165
|
-
let m = /^\/token\/(0x[a-fA-F0-9]{40})-(\d+)\/?$/.exec(url.pathname);
|
|
166
|
-
if (m) {
|
|
167
|
-
return {
|
|
168
|
-
kind: 'token',
|
|
169
|
-
source: 'artblocks',
|
|
170
|
-
coords: { chain: 'ethereum', contract: m[1].toLowerCase(), tokenId: m[2] },
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
m = /^\/marketplace\/collections\/(0x[a-fA-F0-9]{40})\/tokens\/(\d+)\/?$/.exec(url.pathname);
|
|
174
|
-
if (m) {
|
|
175
|
-
return {
|
|
176
|
-
kind: 'token',
|
|
177
|
-
source: 'artblocks',
|
|
178
|
-
coords: { chain: 'ethereum', contract: m[1].toLowerCase(), tokenId: m[2] },
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
m = /^\/collections?\/([a-z0-9][a-z0-9-]*)\/?$/.exec(url.pathname);
|
|
182
|
-
if (m) {
|
|
183
|
-
return { kind: 'ab-collection', slug: m[1] };
|
|
184
|
-
}
|
|
185
|
-
if (url.pathname.startsWith('/projects/')) {
|
|
186
|
-
return {
|
|
187
|
-
kind: 'unsupported',
|
|
188
|
-
reason: 'Art Blocks `/projects/{id}` URLs are legacy and no longer resolve on artblocks.io. ' +
|
|
189
|
-
'Paste the current `/collection/{slug}` URL or a specific token URL.',
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
return {
|
|
193
|
-
kind: 'unsupported',
|
|
194
|
-
reason: `Art Blocks URL not recognized: ${url.pathname}.`,
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* fxhash URL forms (Tezos):
|
|
199
|
-
* fxhash.xyz/gentk/FX1-{KT1...}-{tokenId} (canonical token URL)
|
|
200
|
-
* fxhash.xyz/iteration/{slug} (single iteration; resolves via fxhash API)
|
|
201
|
-
* fxhash.xyz/project/{slug} (series; current UI form)
|
|
202
|
-
* fxhash.xyz/generative/{slug} (series; legacy URL form, both supported)
|
|
203
|
-
* fxhash.xyz/gentk/{numericId} (legacy — needs fxhash API)
|
|
204
|
-
*
|
|
205
|
-
* Series URLs (`/project/`, `/generative/`) resolve to the project's first
|
|
206
|
-
* minted iteration via `generativeToken(slug:)`; Raster then enumerates the
|
|
207
|
-
* rest of the series. EVM-only projects won't be found on the Tezos GraphQL
|
|
208
|
-
* endpoint and surface as a clean "slug not found" error.
|
|
209
|
-
*
|
|
210
|
-
* EVM (FX2-prefixed) gentks are not in scope: the FF indexer does not index
|
|
211
|
-
* fxhash's EVM contracts in v1.
|
|
212
|
-
*/
|
|
213
|
-
function parseFxhash(url) {
|
|
214
|
-
const m = /^\/gentk\/FX1-(KT[A-Za-z0-9]+)-(\d+)\/?$/.exec(url.pathname);
|
|
215
|
-
if (m) {
|
|
216
|
-
return {
|
|
217
|
-
kind: 'token',
|
|
218
|
-
source: 'fxhash',
|
|
219
|
-
coords: { chain: 'tezos', contract: m[1], tokenId: m[2] },
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
if (/^\/gentk\/FX2-/.test(url.pathname)) {
|
|
223
|
-
return {
|
|
224
|
-
kind: 'unsupported',
|
|
225
|
-
reason: 'fxhash FX2 (EVM) tokens are not supported — FF indexer covers Tezos mainnet only ' +
|
|
226
|
-
'for fxhash. Paste a Tezos fxhash token or a different marketplace URL.',
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
if (/^\/gentk\/\d+\/?$/.test(url.pathname)) {
|
|
230
|
-
return {
|
|
231
|
-
kind: 'unsupported',
|
|
232
|
-
reason: 'fxhash legacy numeric gentk URLs require an fxhash API lookup — not supported in v1. ' +
|
|
233
|
-
'Paste the FX1-{contract}-{tokenId} form or use `tezos:{contract}:{tokenId}`.',
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
const iter = /^\/iteration\/([a-z0-9][a-z0-9-]*)\/?$/.exec(url.pathname);
|
|
237
|
-
if (iter) {
|
|
238
|
-
return { kind: 'fxhash-iteration', slug: iter[1] };
|
|
239
|
-
}
|
|
240
|
-
const project = /^\/(?:project|generative)\/([a-z0-9][a-z0-9-]*)\/?$/.exec(url.pathname);
|
|
241
|
-
if (project) {
|
|
242
|
-
return { kind: 'fxhash-project', slug: project[1] };
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
kind: 'unsupported',
|
|
246
|
-
reason: `fxhash URL not recognized: ${url.pathname}.`,
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* OpenSea URL forms:
|
|
251
|
-
* opensea.io/assets/{chain}/{contract}/{tokenId}
|
|
252
|
-
* opensea.io/item/{chain}/{contract}/{tokenId} — newer naming
|
|
253
|
-
* opensea.io/collection/{slug} — collection slug (series)
|
|
254
|
-
*
|
|
255
|
-
* Only `ethereum` is in scope; other chains (matic, base, solana, etc.)
|
|
256
|
-
* fall outside the FF indexer's coverage. Token URLs resolve end-to-end only
|
|
257
|
-
* when Raster also indexes the underlying series — Raster's coverage of
|
|
258
|
-
* mainstream PFP collections (Azuki, BAYC, etc.) is partial.
|
|
259
|
-
*/
|
|
260
|
-
function parseOpenSea(url) {
|
|
261
|
-
const tokenMatch = /^\/(?:assets|item)\/([a-z_]+)\/(0x[a-fA-F0-9]{40})\/(\d+)\/?$/.exec(url.pathname);
|
|
262
|
-
if (tokenMatch) {
|
|
263
|
-
const chain = tokenMatch[1];
|
|
264
|
-
if (chain !== 'ethereum') {
|
|
265
|
-
return {
|
|
266
|
-
kind: 'unsupported',
|
|
267
|
-
reason: `OpenSea ${chain} URLs aren't supported — ff-cli covers Ethereum and Tezos mainnet only. ` +
|
|
268
|
-
'Paste an Ethereum-chain OpenSea URL or a Tezos source (Objkt, fxhash).',
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
return {
|
|
272
|
-
kind: 'token',
|
|
273
|
-
source: 'opensea',
|
|
274
|
-
coords: { chain: 'ethereum', contract: tokenMatch[2].toLowerCase(), tokenId: tokenMatch[3] },
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
const collectionMatch = /^\/collection\/([a-z0-9][a-z0-9-]*)\/?$/.exec(url.pathname);
|
|
278
|
-
if (collectionMatch) {
|
|
279
|
-
return { kind: 'os-collection', slug: collectionMatch[1] };
|
|
280
|
-
}
|
|
281
|
-
if (url.pathname.startsWith('/collection/')) {
|
|
282
|
-
return {
|
|
283
|
-
kind: 'unsupported',
|
|
284
|
-
reason: `OpenSea collection URL not recognized: ${url.pathname}. ` +
|
|
285
|
-
'Expected opensea.io/collection/{slug} with no extra path segments.',
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
return {
|
|
289
|
-
kind: 'unsupported',
|
|
290
|
-
reason: `OpenSea URL not recognized: ${url.pathname}.`,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* SuperRare URL forms (Ethereum-only platform):
|
|
295
|
-
* superrare.com/artwork/eth/{contract}/{tokenId}
|
|
296
|
-
* superrare.com/collection/{contract} — per-artist contract page, not supported in v1
|
|
297
|
-
* superrare.com/{artist}/{slug} — slug form, not supported in v1
|
|
298
|
-
*
|
|
299
|
-
* SuperRare's `/artwork/eth/...` URLs encode both the contract and tokenId
|
|
300
|
-
* directly, so no API call is needed. Token URLs resolve end-to-end only when
|
|
301
|
-
* Raster also indexes the underlying series; for 1/1 artworks the find flow
|
|
302
|
-
* falls through to a single-token playlist via the Raster 404 path.
|
|
303
|
-
*
|
|
304
|
-
* `/collection/{contract}` URLs on SuperRare point to a per-artist contract
|
|
305
|
-
* (each SuperRare artist mints from their own ERC-721). There is no direct
|
|
306
|
-
* contract→series mapping in Raster, so this is surfaced as a specific
|
|
307
|
-
* unsupported message rather than the generic "URL not recognized" fall-through.
|
|
308
|
-
*/
|
|
309
|
-
function parseSuperRare(url) {
|
|
310
|
-
const m = /^\/artwork\/eth\/(0x[a-fA-F0-9]{40})\/(\d+)\/?$/.exec(url.pathname);
|
|
311
|
-
if (m) {
|
|
312
|
-
return {
|
|
313
|
-
kind: 'token',
|
|
314
|
-
source: 'superrare',
|
|
315
|
-
coords: { chain: 'ethereum', contract: m[1].toLowerCase(), tokenId: m[2] },
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
if (/^\/collection\/(0x[a-fA-F0-9]{40})\/?$/.test(url.pathname)) {
|
|
319
|
-
return {
|
|
320
|
-
kind: 'unsupported',
|
|
321
|
-
reason: 'SuperRare `/collection/{contract}` URLs (per-artist contract pages) are not yet ' +
|
|
322
|
-
'supported in v1. Paste a specific token URL ' +
|
|
323
|
-
'(superrare.com/artwork/eth/{contract}/{tokenId}) or use ' +
|
|
324
|
-
'`ethereum:{contract}:{tokenId}`.',
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
kind: 'unsupported',
|
|
329
|
-
reason: `SuperRare URL not recognized: ${url.pathname}. Expected ` +
|
|
330
|
-
'/artwork/eth/{contract}/{tokenId}. For artist-slug URLs, paste the canonical ' +
|
|
331
|
-
'/artwork/eth/... form (the SuperRare detail page links it under the artwork title).',
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Neort URL forms (off-chain code-art platform):
|
|
336
|
-
* neort.io/art/{id} — art detail page; id is opaque alphanumeric (~20 chars)
|
|
337
|
-
*
|
|
338
|
-
* Neort items are not on-chain, so the find flow handles them specially:
|
|
339
|
-
* resolve via Neort's public API and build a DP-1 item directly with
|
|
340
|
-
* `provenance.offChainURI`, bypassing Raster + FF indexer entirely.
|
|
341
|
-
*/
|
|
342
|
-
function parseNeort(url) {
|
|
343
|
-
const m = /^\/art\/([a-zA-Z0-9]+)\/?$/.exec(url.pathname);
|
|
344
|
-
if (m) {
|
|
345
|
-
return { kind: 'neort-art', id: m[1] };
|
|
346
|
-
}
|
|
347
|
-
return {
|
|
348
|
-
kind: 'unsupported',
|
|
349
|
-
reason: `Neort URL not recognized: ${url.pathname}. Expected /art/{id}.`,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Verse URL forms:
|
|
354
|
-
* verse.works/items/ethereum/{contract}/{tokenId} — direct Ethereum token
|
|
355
|
-
* verse.works/series/{slug} — series page
|
|
356
|
-
*
|
|
357
|
-
* Verse item pages expose the on-chain coordinates directly in the path. Series
|
|
358
|
-
* pages do not include a JSON API in the public URL, but they render edition
|
|
359
|
-
* links with the same `/items/ethereum/...` shape; the async resolver fetches
|
|
360
|
-
* the series page and extracts a representative token for Raster enumeration.
|
|
361
|
-
*/
|
|
362
|
-
function parseVerse(url) {
|
|
363
|
-
let m = /^\/items\/ethereum\/(0x[a-fA-F0-9]{40})\/(\d+)\/?$/.exec(url.pathname);
|
|
364
|
-
if (m) {
|
|
365
|
-
return {
|
|
366
|
-
kind: 'token',
|
|
367
|
-
source: 'verse',
|
|
368
|
-
coords: { chain: 'ethereum', contract: m[1].toLowerCase(), tokenId: m[2] },
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
|
-
m = /^\/items\/([^/]+)\//.exec(url.pathname);
|
|
372
|
-
if (m) {
|
|
373
|
-
return {
|
|
374
|
-
kind: 'unsupported',
|
|
375
|
-
reason: `Verse ${m[1]} item URLs are not supported — ff-cli covers Ethereum and Tezos mainnet only. ` +
|
|
376
|
-
'Paste an Ethereum Verse item URL or raw `ethereum:{contract}:{tokenId}` coordinates.',
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
m = /^\/series\/([A-Za-z0-9][A-Za-z0-9_-]*)\/?$/.exec(url.pathname);
|
|
380
|
-
if (m) {
|
|
381
|
-
return { kind: 'verse-series', slug: m[1] };
|
|
382
|
-
}
|
|
383
|
-
return {
|
|
384
|
-
kind: 'unsupported',
|
|
385
|
-
reason: `Verse URL not recognized: ${url.pathname}. Expected ` +
|
|
386
|
-
'/items/ethereum/{contract}/{tokenId} or /series/{slug}.',
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Raster URL forms (raster.art):
|
|
391
|
-
* raster.art/artwork/{slug} — artwork (series) detail page
|
|
392
|
-
*
|
|
393
|
-
* Unlike the other marketplaces, Raster is already the find flow's backend
|
|
394
|
-
* resolver — so a Raster URL resolves to an artwork by slug via
|
|
395
|
-
* `artworkBySlug` and (crucially) builds DP-1 items straight from Raster's
|
|
396
|
-
* own `media.contentUrl`, bypassing the FF indexer the way Neort does. This
|
|
397
|
-
* is what lets Raster-minted tokens (which the FF indexer doesn't carry)
|
|
398
|
-
* build a verifiable playlist without `--skip-verify`.
|
|
399
|
-
*/
|
|
400
|
-
function parseRaster(url) {
|
|
401
|
-
const m = /^\/artwork\/([A-Za-z0-9][A-Za-z0-9_-]*)\/?$/.exec(url.pathname);
|
|
402
|
-
if (m) {
|
|
403
|
-
return { kind: 'raster-artwork', slug: m[1] };
|
|
404
|
-
}
|
|
405
|
-
return {
|
|
406
|
-
kind: 'unsupported',
|
|
407
|
-
reason: `Raster URL not recognized: ${url.pathname}. Expected /artwork/{slug}.`,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Feral File URL forms (source of truth: feralfile-client app-routing
|
|
412
|
-
* + collection-routing modules):
|
|
413
|
-
* /exhibitions/artwork/{tokenId} — single artwork by on-chain tokenId
|
|
414
|
-
* /exhibitions/series/{slug} — series detail (multi-token)
|
|
415
|
-
* /exhibitions/shows/{slug} — full exhibition (multiple series)
|
|
416
|
-
*
|
|
417
|
-
* These URLs do not include `chain` or `contract`; FF API walks are required
|
|
418
|
-
* to derive them. The parser stays sync — it returns an `ff-url` marker that
|
|
419
|
-
* the caller resolves asynchronously via `ff-marketplace#resolveFeralFileToken`.
|
|
420
|
-
* Tracked for collapse to a single GET in feral-file/ff-exhibition#3039.
|
|
421
|
-
*/
|
|
422
|
-
function parseFeralFile(url) {
|
|
423
|
-
// Public artwork ids may be numeric (legacy: URL id == on-chain tokenID) or
|
|
424
|
-
// hex hashes (swapped-id editions). Accept any URL-safe alphanumeric form;
|
|
425
|
-
// the FF API rejects unknown ids cleanly downstream.
|
|
426
|
-
let m = /^\/exhibitions\/artwork\/([A-Za-z0-9]+)\/?$/.exec(url.pathname);
|
|
427
|
-
if (m) {
|
|
428
|
-
return { kind: 'ff-url', urlKind: 'artwork', identifier: m[1] };
|
|
429
|
-
}
|
|
430
|
-
m = /^\/exhibitions\/series\/([^/]+)\/?$/.exec(url.pathname);
|
|
431
|
-
if (m) {
|
|
432
|
-
return { kind: 'ff-url', urlKind: 'series', identifier: m[1] };
|
|
433
|
-
}
|
|
434
|
-
m = /^\/exhibitions\/shows\/([^/]+)\/?$/.exec(url.pathname);
|
|
435
|
-
if (m) {
|
|
436
|
-
return { kind: 'ff-url', urlKind: 'show', identifier: m[1] };
|
|
437
|
-
}
|
|
438
|
-
return {
|
|
439
|
-
kind: 'unsupported',
|
|
440
|
-
reason: `Feral File URL not recognized: ${url.pathname}. Supported: ` +
|
|
441
|
-
'/exhibitions/artwork/{tokenId}, /exhibitions/series/{slug}, /exhibitions/shows/{slug}.',
|
|
442
|
-
};
|
|
443
|
-
}
|