@feralfile/cli 1.4.0 → 1.5.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/dist/package.json +1 -1
- package/dist/src/commands/find.js +52 -25
- package/dist/src/utilities/raster-client.js +126 -127
- package/docs/CONFIGURATION.md +13 -0
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -109,24 +109,40 @@ exports.findCommand = new commander_1.Command('find')
|
|
|
109
109
|
}
|
|
110
110
|
console.log();
|
|
111
111
|
const userLimit = parseLimitOption(options.limit);
|
|
112
|
-
const seriesTotal = target.kind === 'series' ? target.summary.tokenCount : 1;
|
|
113
112
|
// Implicit cap: no --limit + series > 1024 → cap to DP-1 max with a
|
|
114
113
|
// clear warning. Explicit `--limit > 1024` was already rejected by
|
|
115
114
|
// parseLimitOption, so reaching here means either capped-by-default or
|
|
116
115
|
// the user picked a value within spec.
|
|
117
116
|
const limit = Math.min(userLimit, DP1_MAX_ITEMS);
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
// Raster's GraphQL API exposes no series token counts, so enumerate
|
|
118
|
+
// tokens first (up to the effective limit) and confirm with the real
|
|
119
|
+
// number afterward. `hasMore` means the series continues past `limit`.
|
|
120
|
+
let tokens;
|
|
121
|
+
let seriesHasMore = false;
|
|
122
|
+
if (target.kind === 'series') {
|
|
123
|
+
const fetched = await fetchTokens(target.summary, limit);
|
|
124
|
+
tokens = fetched.tokens;
|
|
125
|
+
seriesHasMore = fetched.hasMore;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
tokens = [target.coords];
|
|
129
|
+
}
|
|
130
|
+
if (tokens.length === 0) {
|
|
131
|
+
console.error(chalk_1.default.red('Series has no tokens on supported chains (Ethereum + Tezos mainnet).'));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
if (userLimit === Number.POSITIVE_INFINITY && seriesHasMore) {
|
|
135
|
+
console.log(chalk_1.default.yellow(`Series has more than ${DP1_MAX_ITEMS} tokens; DP-1 caps playlists at ${DP1_MAX_ITEMS}. ` +
|
|
120
136
|
`Building with the first ${DP1_MAX_ITEMS} — pass \`--limit N\` (≤ ${DP1_MAX_ITEMS}) for fewer.`));
|
|
121
137
|
console.log();
|
|
122
138
|
}
|
|
123
|
-
const
|
|
124
|
-
|
|
139
|
+
const shouldBuild = !!options.yes ||
|
|
140
|
+
!!options.output ||
|
|
141
|
+
(await confirmMakePlaylist(tokens.length, seriesHasMore));
|
|
125
142
|
if (!shouldBuild) {
|
|
126
143
|
console.log(chalk_1.default.dim('Cancelled.'));
|
|
127
144
|
return;
|
|
128
145
|
}
|
|
129
|
-
const tokens = target.kind === 'series' ? await fetchTokens(target.summary, limit) : [target.coords];
|
|
130
146
|
console.log(chalk_1.default.dim(`Indexing ${tokens.length} token${tokens.length === 1 ? '' : 's'} via FF indexer...`));
|
|
131
147
|
// Second positional arg on getNFTTokenInfoBatch is `duration` (DP-1 item
|
|
132
148
|
// display seconds), not concurrency — concurrency is hardcoded inside.
|
|
@@ -202,32 +218,33 @@ async function resolveTarget(parsed, skipPrompt) {
|
|
|
202
218
|
return resolveCoords(coords);
|
|
203
219
|
}
|
|
204
220
|
if (parsed.kind === 'address') {
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
221
|
+
const artist = await (0, raster_client_1.resolveAddressToArtist)(parsed.address);
|
|
222
|
+
if (artist === null) {
|
|
207
223
|
throw new Error(`No artist found on Raster for address ${parsed.address}. ` +
|
|
208
224
|
'Raster indexes artists by their on-chain addresses; this address may not yet be claimed.');
|
|
209
225
|
}
|
|
210
|
-
const catalog = await (0, raster_client_1.listArtistArtworks)(
|
|
226
|
+
const catalog = await (0, raster_client_1.listArtistArtworks)(artist.id);
|
|
211
227
|
if (catalog.length === 0) {
|
|
212
228
|
throw new Error('Artist has no artworks indexed on Raster.');
|
|
213
229
|
}
|
|
214
230
|
const picked = await pickFromCatalog(catalog, skipPrompt);
|
|
215
|
-
|
|
216
|
-
|
|
231
|
+
return {
|
|
232
|
+
kind: 'series',
|
|
233
|
+
summary: { artworkId: picked.artworkId, title: picked.title, artists: [artist] },
|
|
234
|
+
};
|
|
217
235
|
}
|
|
218
236
|
// `unsupported` is handled upstream; this branch keeps the type narrowing honest.
|
|
219
237
|
throw new Error(`Unsupported parse result: ${parsed.kind}`);
|
|
220
238
|
}
|
|
221
239
|
/**
|
|
222
|
-
* Resolve on-chain coords to a target. If Raster doesn't index this token
|
|
223
|
-
*
|
|
240
|
+
* Resolve on-chain coords to a target. If Raster doesn't index this token,
|
|
241
|
+
* fall back to single-token mode so we still build something playable.
|
|
224
242
|
*/
|
|
225
243
|
async function resolveCoords(coords) {
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
244
|
+
const summary = await (0, raster_client_1.resolveTokenToArtwork)(coords.chain, coords.contract, coords.tokenId);
|
|
245
|
+
if (summary === null) {
|
|
228
246
|
return { kind: 'single', coords };
|
|
229
247
|
}
|
|
230
|
-
const summary = await (0, raster_client_1.getArtworkSummary)(lookup.artworkId);
|
|
231
248
|
return { kind: 'series', summary };
|
|
232
249
|
}
|
|
233
250
|
/**
|
|
@@ -258,11 +275,17 @@ function parseLimitOption(limitStr) {
|
|
|
258
275
|
}
|
|
259
276
|
return n;
|
|
260
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Enumerate series tokens from Raster up to `limit`. `hasMore` reports
|
|
280
|
+
* whether the series continues past what was collected — the API exposes
|
|
281
|
+
* no total counts, so this is all the size information the flow gets.
|
|
282
|
+
*/
|
|
261
283
|
async function fetchTokens(summary, limit) {
|
|
262
284
|
const collected = [];
|
|
263
285
|
let cursor;
|
|
264
286
|
let skipped = 0;
|
|
265
|
-
|
|
287
|
+
let hasMore = false;
|
|
288
|
+
for (;;) {
|
|
266
289
|
const page = await (0, raster_client_1.listArtworkTokens)(summary.artworkId, {
|
|
267
290
|
cursor,
|
|
268
291
|
pageSize: RASTER_PAGE_SIZE,
|
|
@@ -270,6 +293,7 @@ async function fetchTokens(summary, limit) {
|
|
|
270
293
|
skipped += page.skippedUnsupported;
|
|
271
294
|
for (const t of page.tokens) {
|
|
272
295
|
if (collected.length >= limit) {
|
|
296
|
+
hasMore = true;
|
|
273
297
|
break;
|
|
274
298
|
}
|
|
275
299
|
collected.push({
|
|
@@ -278,7 +302,11 @@ async function fetchTokens(summary, limit) {
|
|
|
278
302
|
tokenId: t.tokenId,
|
|
279
303
|
});
|
|
280
304
|
}
|
|
281
|
-
if (page.nextCursor === null) {
|
|
305
|
+
if (hasMore || page.nextCursor === null) {
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
if (collected.length >= limit) {
|
|
309
|
+
hasMore = true;
|
|
282
310
|
break;
|
|
283
311
|
}
|
|
284
312
|
cursor = page.nextCursor;
|
|
@@ -286,7 +314,7 @@ async function fetchTokens(summary, limit) {
|
|
|
286
314
|
if (skipped > 0) {
|
|
287
315
|
console.log(chalk_1.default.yellow(` Skipped ${skipped} token${skipped === 1 ? '' : 's'} on unsupported chains (FF indexer covers eth + tezos mainnet only).`));
|
|
288
316
|
}
|
|
289
|
-
return collected;
|
|
317
|
+
return { tokens: collected, hasMore };
|
|
290
318
|
}
|
|
291
319
|
async function pickFromCatalog(catalog, skipPrompt) {
|
|
292
320
|
if (catalog.length === 1 || skipPrompt) {
|
|
@@ -294,8 +322,7 @@ async function pickFromCatalog(catalog, skipPrompt) {
|
|
|
294
322
|
}
|
|
295
323
|
console.log(chalk_1.default.cyan(`Found ${catalog.length} artworks for this artist:`));
|
|
296
324
|
catalog.forEach((row, i) => {
|
|
297
|
-
|
|
298
|
-
console.log(chalk_1.default.dim(` ${i}: ${row.title} (${count})`));
|
|
325
|
+
console.log(chalk_1.default.dim(` ${i}: ${row.title}`));
|
|
299
326
|
});
|
|
300
327
|
console.log();
|
|
301
328
|
const prompt = (0, prompt_1.createPrompt)();
|
|
@@ -310,9 +337,9 @@ async function pickFromCatalog(catalog, skipPrompt) {
|
|
|
310
337
|
}
|
|
311
338
|
return catalog[index];
|
|
312
339
|
}
|
|
313
|
-
async function confirmMakePlaylist(count,
|
|
314
|
-
const noun =
|
|
315
|
-
const label =
|
|
340
|
+
async function confirmMakePlaylist(count, hasMore) {
|
|
341
|
+
const noun = count === 1 ? 'token' : 'tokens';
|
|
342
|
+
const label = hasMore ? `the first ${count} ${noun}` : `${count} ${noun}`;
|
|
316
343
|
const prompt = (0, prompt_1.createPrompt)();
|
|
317
344
|
const yes = await (0, prompt_1.promptYesNo)(prompt.ask, `Build playlist with ${label}?`, true);
|
|
318
345
|
prompt.close();
|
|
@@ -488,7 +515,7 @@ async function runNeortFind(id, options) {
|
|
|
488
515
|
console.log(chalk_1.default.cyan(`Neort — ${artistLabel} — ${art.title}`));
|
|
489
516
|
console.log(chalk_1.default.dim(' (Off-chain platform; building a one-item playlist directly from Neort.)'));
|
|
490
517
|
console.log();
|
|
491
|
-
const shouldBuild = !!options.yes || !!options.output || (await confirmMakePlaylist(1,
|
|
518
|
+
const shouldBuild = !!options.yes || !!options.output || (await confirmMakePlaylist(1, false));
|
|
492
519
|
if (!shouldBuild) {
|
|
493
520
|
console.log(chalk_1.default.dim('Cancelled.'));
|
|
494
521
|
return;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Raster API client (
|
|
3
|
+
* Raster API client (api.raster.art — public GraphQL API).
|
|
4
4
|
*
|
|
5
|
-
* Resolves on-chain coordinates and artist addresses into Raster
|
|
6
|
-
* (series) and
|
|
5
|
+
* Resolves on-chain coordinates and artist addresses into Raster artworks
|
|
6
|
+
* (series) and artists (catalog) — the inputs the `ff-cli find` flow needs
|
|
7
7
|
* before handing tokens to the FF GraphQL indexer for DP-1 conversion.
|
|
8
8
|
*
|
|
9
9
|
* This module is pure data: it does not construct DP-1 items, send anything
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* mainnet. Token lists are filtered here at the CAIP level and a
|
|
15
15
|
* `skippedUnsupported` count is surfaced so callers can warn-skip without
|
|
16
16
|
* walking every row themselves.
|
|
17
|
+
*
|
|
18
|
+
* API notes (v0.1.0, https://docs.api.raster.art/):
|
|
19
|
+
* - Auth: `x-api-key` header, sent when RASTER_API_KEY is set in the
|
|
20
|
+
* environment. The endpoint currently answers without a key; the docs
|
|
21
|
+
* declare one required, so the hook is wired in ahead of enforcement.
|
|
22
|
+
* - Not-found is a `null` query field, not an HTTP 404.
|
|
23
|
+
* - Connections expose no total counts — callers learn series size by
|
|
24
|
+
* paginating, so the find flow fetches before it prompts.
|
|
17
25
|
*/
|
|
18
26
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
27
|
if (k2 === undefined) k2 = k;
|
|
@@ -49,130 +57,112 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
49
57
|
};
|
|
50
58
|
})();
|
|
51
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
-
exports.RasterNotFoundError = void 0;
|
|
53
60
|
exports.resolveTokenToArtwork = resolveTokenToArtwork;
|
|
54
|
-
exports.getArtworkSummary = getArtworkSummary;
|
|
55
61
|
exports.resolveAddressToArtist = resolveAddressToArtist;
|
|
56
62
|
exports.listArtworkTokens = listArtworkTokens;
|
|
57
63
|
exports.listArtistArtworks = listArtistArtworks;
|
|
58
64
|
exports.formatSummaryLine = formatSummaryLine;
|
|
59
65
|
const logger = __importStar(require("../logger"));
|
|
60
66
|
const user_agent_1 = require("./user-agent");
|
|
61
|
-
const
|
|
67
|
+
const DEFAULT_RASTER_GRAPHQL_URL = 'https://api.raster.art/graphql';
|
|
62
68
|
// CAIP-2 chain IDs for the chains the FF indexer supports.
|
|
63
69
|
// Tezos mainnet's CAIP form uses the genesis block hash (NetXdQprcVkpaWU),
|
|
64
70
|
// not the human-readable "tezos:mainnet" — Raster returns the hash form on
|
|
65
|
-
// token rows and
|
|
71
|
+
// token rows and resolves nothing for "tezos:mainnet".
|
|
66
72
|
const CAIP_TO_INDEXER_CHAIN = {
|
|
67
73
|
'eip155:1': 'ethereum',
|
|
68
74
|
'tezos:NetXdQprcVkpaWU': 'tezos',
|
|
69
75
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
76
|
+
const INDEXER_CHAIN_TO_CAIP = {
|
|
77
|
+
ethereum: 'eip155:1',
|
|
78
|
+
tezos: 'tezos:NetXdQprcVkpaWU',
|
|
79
|
+
};
|
|
74
80
|
function caipToIndexerChain(caip) {
|
|
75
81
|
return CAIP_TO_INDEXER_CHAIN[caip] ?? null;
|
|
76
82
|
}
|
|
77
83
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
84
|
+
* POST one GraphQL operation to Raster and return `data`.
|
|
85
|
+
*
|
|
86
|
+
* Throws on HTTP errors and on GraphQL `errors` entries. "Not found" never
|
|
87
|
+
* lands here — Raster models it as a null query field with no error, so
|
|
88
|
+
* callers check their own field for null.
|
|
81
89
|
*/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
async function rasterQuery(query, variables) {
|
|
91
|
+
const endpoint = process.env.RASTER_API_URL ?? DEFAULT_RASTER_GRAPHQL_URL;
|
|
92
|
+
logger.debug(`[Raster] POST ${endpoint} ${JSON.stringify(variables)}`);
|
|
93
|
+
const headers = {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
'User-Agent': user_agent_1.USER_AGENT,
|
|
96
|
+
};
|
|
97
|
+
const apiKey = process.env.RASTER_API_KEY;
|
|
98
|
+
if (apiKey) {
|
|
99
|
+
headers['x-api-key'] = apiKey;
|
|
87
100
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
logger.debug(`[Raster] GET ${url}`);
|
|
93
|
-
const response = await fetch(url, {
|
|
94
|
-
headers: { 'User-Agent': user_agent_1.USER_AGENT, Accept: 'application/json' },
|
|
101
|
+
const response = await fetch(endpoint, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers,
|
|
104
|
+
body: JSON.stringify({ query, variables }),
|
|
95
105
|
});
|
|
96
|
-
if (response.status === 404) {
|
|
97
|
-
throw new RasterNotFoundError(path);
|
|
98
|
-
}
|
|
99
106
|
if (!response.ok) {
|
|
100
107
|
const body = await response.text();
|
|
101
|
-
throw new Error(`Raster API ${response.status} ${response.statusText}
|
|
108
|
+
throw new Error(`Raster API ${response.status} ${response.statusText}: ${body.slice(0, 200)}`);
|
|
109
|
+
}
|
|
110
|
+
const payload = (await response.json());
|
|
111
|
+
if (payload.errors && payload.errors.length > 0) {
|
|
112
|
+
throw new Error(`Raster API error: ${payload.errors.map((e) => e.message).join('; ')}`);
|
|
113
|
+
}
|
|
114
|
+
if (payload.data === undefined) {
|
|
115
|
+
throw new Error('Raster API returned no data.');
|
|
102
116
|
}
|
|
103
|
-
return
|
|
117
|
+
return payload.data;
|
|
118
|
+
}
|
|
119
|
+
function toRasterArtist(raw) {
|
|
120
|
+
return { id: raw.id, name: raw.name ?? 'Unknown artist', slug: raw.slug };
|
|
104
121
|
}
|
|
105
122
|
/**
|
|
106
|
-
* Resolve a token's on-chain coordinates to its Raster artwork (series)
|
|
123
|
+
* Resolve a token's on-chain coordinates to its Raster artwork (series).
|
|
107
124
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
125
|
+
* One round trip: `tokenByRef` returns the artwork with artists inline, so
|
|
126
|
+
* this yields the full summary directly. Returns `null` when Raster doesn't
|
|
127
|
+
* index this token; callers fall back to a single-token playlist in that
|
|
128
|
+
* case. Tokens occasionally belong to multiple artworks; the first one is
|
|
129
|
+
* Raster's primary association.
|
|
112
130
|
*/
|
|
113
131
|
async function resolveTokenToArtwork(chain, contract, tokenId) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
throw error;
|
|
132
|
+
const data = await rasterQuery(`query ResolveToken($ref: TokenRefInput!) {
|
|
133
|
+
tokenByRef(ref: $ref) {
|
|
134
|
+
artworks { id title artists { id name slug } }
|
|
135
|
+
}
|
|
136
|
+
}`, {
|
|
137
|
+
ref: {
|
|
138
|
+
chainId: INDEXER_CHAIN_TO_CAIP[chain],
|
|
139
|
+
contractAddress: contract,
|
|
140
|
+
tokenId,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const artwork = data.tokenByRef?.artworks?.[0];
|
|
144
|
+
if (!artwork) {
|
|
145
|
+
return null;
|
|
129
146
|
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Get the full Raster artwork summary suitable for the one-line resolve
|
|
133
|
-
* message ("Snowfro — send/receive — 8977 tokens") and downstream UX.
|
|
134
|
-
*/
|
|
135
|
-
async function getArtworkSummary(artworkId) {
|
|
136
|
-
const data = await rasterFetch(`/artwork/${artworkId}`);
|
|
137
147
|
return {
|
|
138
|
-
artworkId:
|
|
139
|
-
title:
|
|
140
|
-
artists:
|
|
141
|
-
tokenCount: data.token_count,
|
|
142
|
-
editionSize: data.edition_size,
|
|
143
|
-
isEdition: data.is_edition,
|
|
144
|
-
hasOneOfOneTokens: data.has_one_of_one_tokens,
|
|
145
|
-
platforms: data.platforms ?? [],
|
|
146
|
-
contractAddresses: data.contract_addresses ?? [],
|
|
148
|
+
artworkId: artwork.id,
|
|
149
|
+
title: artwork.title,
|
|
150
|
+
artists: (artwork.artists ?? []).map(toRasterArtist),
|
|
147
151
|
};
|
|
148
152
|
}
|
|
149
153
|
/**
|
|
150
|
-
* Resolve
|
|
154
|
+
* Resolve a wallet address to the Raster artist who claims it.
|
|
151
155
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
156
|
+
* The `address` query carries artist associations directly (the old REST
|
|
157
|
+
* API needed a `/search` bridge). Returns `null` when no artist claims the
|
|
158
|
+
* address. Checksummed Ethereum input is normalized server-side.
|
|
155
159
|
*/
|
|
156
160
|
async function resolveAddressToArtist(address) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (doc.data_type !== 'artist') {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
const addrs = (doc.addresses ?? []).map((a) => a.toLowerCase());
|
|
166
|
-
if (!addrs.includes(normalized)) {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const parsed = Number.parseInt(doc.data_pk, 10);
|
|
170
|
-
if (Number.isFinite(parsed)) {
|
|
171
|
-
return parsed;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return null;
|
|
161
|
+
const data = await rasterQuery(`query ResolveAddress($address: String!) {
|
|
162
|
+
address(address: $address) { artists { id name slug } }
|
|
163
|
+
}`, { address: address.trim() });
|
|
164
|
+
const artist = data.address?.artists?.[0];
|
|
165
|
+
return artist ? toRasterArtist(artist) : null;
|
|
176
166
|
}
|
|
177
167
|
/**
|
|
178
168
|
* List tokens in a Raster artwork (series), one page at a time.
|
|
@@ -183,70 +173,79 @@ async function resolveAddressToArtist(address) {
|
|
|
183
173
|
* warn-skip line instead of one per token.
|
|
184
174
|
*
|
|
185
175
|
* Pagination: pass the returned `nextCursor` back as `options.cursor`.
|
|
186
|
-
* `nextCursor` is `null` on the final page.
|
|
187
|
-
*
|
|
176
|
+
* `nextCursor` is `null` on the final page. The API exposes no total count;
|
|
177
|
+
* the only way to learn a series' size is to walk it.
|
|
188
178
|
*/
|
|
189
179
|
async function listArtworkTokens(artworkId, options = {}) {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
180
|
+
const data = await rasterQuery(`query ArtworkTokens($id: ID!, $first: Int, $after: String) {
|
|
181
|
+
artwork(id: $id) {
|
|
182
|
+
tokens(first: $first, after: $after) {
|
|
183
|
+
nodes { chainId contractAddress tokenId }
|
|
184
|
+
pageInfo { hasNextPage endCursor }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}`, { id: artworkId, first: options.pageSize ?? null, after: options.cursor ?? null });
|
|
188
|
+
if (!data.artwork) {
|
|
189
|
+
throw new Error(`Raster: artwork ${artworkId} not found.`);
|
|
196
190
|
}
|
|
197
|
-
const qs = params.toString();
|
|
198
|
-
const path = `/artwork/${artworkId}/tokens${qs ? `?${qs}` : ''}`;
|
|
199
|
-
const data = await rasterFetch(path);
|
|
200
191
|
const tokens = [];
|
|
201
192
|
let skippedUnsupported = 0;
|
|
202
|
-
for (const raw of data.tokens
|
|
203
|
-
const chain = caipToIndexerChain(raw.
|
|
204
|
-
if (!chain) {
|
|
193
|
+
for (const raw of data.artwork.tokens.nodes) {
|
|
194
|
+
const chain = caipToIndexerChain(raw.chainId);
|
|
195
|
+
if (!chain || !raw.contractAddress) {
|
|
205
196
|
skippedUnsupported += 1;
|
|
206
197
|
continue;
|
|
207
198
|
}
|
|
208
199
|
tokens.push({
|
|
209
|
-
caipChain: raw.chain_id,
|
|
210
200
|
chain,
|
|
211
|
-
contractAddress:
|
|
212
|
-
tokenId: raw.
|
|
201
|
+
contractAddress: raw.contractAddress,
|
|
202
|
+
tokenId: raw.tokenId,
|
|
213
203
|
});
|
|
214
204
|
}
|
|
215
|
-
const
|
|
216
|
-
const nextCursor = rawCursor === null || rawCursor >= data.total_count ? null : rawCursor;
|
|
205
|
+
const { hasNextPage, endCursor } = data.artwork.tokens.pageInfo;
|
|
217
206
|
return {
|
|
218
207
|
tokens,
|
|
219
|
-
|
|
220
|
-
nextCursor,
|
|
208
|
+
nextCursor: hasNextPage && endCursor ? endCursor : null,
|
|
221
209
|
skippedUnsupported,
|
|
222
210
|
};
|
|
223
211
|
}
|
|
224
212
|
/**
|
|
225
|
-
* List an artist's full catalog as light artwork rows (id
|
|
226
|
-
*
|
|
227
|
-
* are needed for a specific row.
|
|
213
|
+
* List an artist's full catalog as light artwork rows (id + title — the
|
|
214
|
+
* API exposes no per-artwork token counts or mint dates yet).
|
|
228
215
|
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
216
|
+
* Walks the artist's `artworks` connection to the end so the caller sees
|
|
217
|
+
* the complete catalog, matching the old single-response REST behavior.
|
|
231
218
|
*/
|
|
232
219
|
async function listArtistArtworks(artistId) {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
220
|
+
const rows = [];
|
|
221
|
+
let cursor = null;
|
|
222
|
+
do {
|
|
223
|
+
const data = await rasterQuery(`query ArtistArtworks($id: ID!, $after: String) {
|
|
224
|
+
artist(id: $id) {
|
|
225
|
+
artworks(first: 100, after: $after) {
|
|
226
|
+
nodes { id title }
|
|
227
|
+
pageInfo { hasNextPage endCursor }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}`, { id: artistId, after: cursor });
|
|
231
|
+
if (!data.artist) {
|
|
232
|
+
throw new Error(`Raster: artist ${artistId} not found.`);
|
|
233
|
+
}
|
|
234
|
+
for (const node of data.artist.artworks.nodes) {
|
|
235
|
+
rows.push({ artworkId: node.id, title: node.title });
|
|
236
|
+
}
|
|
237
|
+
const { hasNextPage, endCursor } = data.artist.artworks.pageInfo;
|
|
238
|
+
cursor = hasNextPage && endCursor ? endCursor : null;
|
|
239
|
+
} while (cursor !== null);
|
|
240
|
+
return rows;
|
|
241
241
|
}
|
|
242
242
|
/**
|
|
243
243
|
* Format a one-line summary for the resolved artwork.
|
|
244
244
|
*
|
|
245
245
|
* @example
|
|
246
|
-
* formatSummaryLine(summary) // "Snowfro — send/receive
|
|
246
|
+
* formatSummaryLine(summary) // "Snowfro — send/receive"
|
|
247
247
|
*/
|
|
248
248
|
function formatSummaryLine(summary) {
|
|
249
249
|
const artistNames = summary.artists.map((a) => a.name).join(', ') || 'Unknown artist';
|
|
250
|
-
|
|
251
|
-
return `${artistNames} — ${summary.title} — ${summary.tokenCount} ${tokenWord}`;
|
|
250
|
+
return `${artistNames} — ${summary.title}`;
|
|
252
251
|
}
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -116,6 +116,19 @@ Environment variable alternative:
|
|
|
116
116
|
FEED_BASE_URLS=https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1,https://dp1-feed-operator-api-prod.autonomy-system.workers.dev/api/v1
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
## find (Raster API)
|
|
120
|
+
|
|
121
|
+
`ff-cli find` resolves tokens, series, and artist addresses through Raster's public GraphQL API ([docs.api.raster.art](https://docs.api.raster.art/)). No configuration is required; two environment variables adjust the integration:
|
|
122
|
+
|
|
123
|
+
```env
|
|
124
|
+
# API key sent as the x-api-key header. The API currently answers without
|
|
125
|
+
# one, but Raster's docs declare a key required — set this once enforced.
|
|
126
|
+
RASTER_API_KEY=rk_your_key_here
|
|
127
|
+
|
|
128
|
+
# Override the GraphQL endpoint (default: https://api.raster.art/graphql).
|
|
129
|
+
RASTER_API_URL=https://api.raster.art/graphql
|
|
130
|
+
```
|
|
131
|
+
|
|
119
132
|
## ff1Devices
|
|
120
133
|
|
|
121
134
|
Configure devices you want to play content on.
|