@feralfile/cli 2.10.1 → 4.0.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/README.md +31 -23
- package/config.json.example +6 -1
- package/dist/index.js +10 -2
- package/dist/package.json +5 -2
- package/dist/src/commands/build.js +40 -0
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/device.js +85 -40
- package/dist/src/commands/enrich.js +18 -23
- package/dist/src/commands/find.js +144 -106
- package/dist/src/commands/helpers/device-control.js +937 -0
- package/dist/src/commands/helpers/hidden-alias-help.js +37 -0
- package/dist/src/commands/helpers/playlist-display.js +2 -15
- package/dist/src/commands/helpers/prompt.js +85 -10
- package/dist/src/commands/play.js +162 -84
- package/dist/src/commands/report.js +251 -0
- package/dist/src/commands/setup.js +82 -7
- package/dist/src/commands/status.js +4 -1
- package/dist/src/config.js +256 -6
- package/dist/src/main.js +14 -5
- package/dist/src/utilities/control/codec.js +578 -0
- package/dist/src/utilities/control/control-errors.js +157 -0
- package/dist/src/utilities/control/controller-store.js +964 -0
- package/dist/src/utilities/control/jwk.js +141 -0
- package/dist/src/utilities/control/pairing-channel.js +52 -0
- package/dist/src/utilities/control/pairing.js +416 -0
- package/dist/src/utilities/control/registration.js +391 -0
- package/dist/src/utilities/control/relay-pairing-channel.js +191 -0
- package/dist/src/utilities/control/secure-transport.js +850 -0
- package/dist/src/utilities/control/strict-json.js +376 -0
- package/dist/src/utilities/device-lookup.js +36 -11
- package/dist/src/utilities/device-network.js +129 -0
- package/dist/src/utilities/device-upsert.js +1 -1
- package/dist/src/utilities/feral-file-artwork.js +2 -2
- package/dist/src/utilities/ff1-compatibility.js +87 -11
- package/dist/src/utilities/ff1-device.js +161 -78
- package/dist/src/utilities/ff1-relayer.js +34 -14
- package/dist/src/utilities/handoff-client.js +23 -9
- package/dist/src/utilities/index.js +270 -259
- package/dist/src/utilities/media-url.js +117 -0
- package/dist/src/utilities/neort-marketplace.js +3 -3
- package/dist/src/utilities/nft-indexer.js +12 -376
- package/dist/src/utilities/playlist-builder.js +11 -2
- package/dist/src/utilities/playlist-cast.js +6 -6
- package/dist/src/utilities/playlist-source.js +223 -34
- package/dist/src/utilities/raster-client.js +101 -14
- package/dist/src/utilities/relay-client.js +774 -0
- package/dist/src/utilities/ssh-access.js +192 -58
- package/dist/src/utilities/support-report.js +511 -0
- package/dist/src/utilities/temporary-channel.js +139 -0
- package/dist/src/utilities/token-item.js +428 -0
- package/dist/src/utilities/token-source-contract.js +55 -0
- package/dist/src/utilities/token-source-indexer.js +99 -0
- package/dist/src/utilities/token-source-raster.js +378 -0
- package/dist/src/utilities/token-source.js +48 -0
- package/docs/CONFIGURATION.md +331 -28
- package/docs/EXAMPLES.md +33 -33
- package/docs/PROJECT_SPEC.md +55 -36
- package/docs/README.md +145 -43
- package/package.json +5 -2
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Token item construction shared by every token source.
|
|
4
|
+
*
|
|
5
|
+
* This module turns a StandardToken, the vendor-neutral token shape, into a
|
|
6
|
+
* DP-1 playlist item. It used to live inside nft-indexer.js, which meant the
|
|
7
|
+
* item id, provenance, inline manifest and timing rules would have been
|
|
8
|
+
* deleted along with the ff-indexer-v2 client if Raster replaces it
|
|
9
|
+
* (feral-file#3576). Keeping them here, with no knowledge of any vendor, is
|
|
10
|
+
* what lets two providers emit byte-identical items for the same token: the
|
|
11
|
+
* indexer provider and the Raster provider both map their rows to
|
|
12
|
+
* a StandardToken and hand it to convertToDP1Item.
|
|
13
|
+
*
|
|
14
|
+
* Invariants that must not change without a migration of existing playlists:
|
|
15
|
+
* - the item id is sha256(`${contractAddress}-${tokenId}`) formatted as a
|
|
16
|
+
* UUID, so the same token always yields the same id across vendors;
|
|
17
|
+
* - provenance.contract.chain comes from the row's CAIP-2 chain when the
|
|
18
|
+
* row has one, and only falls back to the caller's chain name otherwise;
|
|
19
|
+
* - the inline manifest id is derived from the token coordinate by
|
|
20
|
+
* ref-manifest.ts, independent of which vendor supplied the metadata.
|
|
21
|
+
*/
|
|
22
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
25
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
26
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
27
|
+
}
|
|
28
|
+
Object.defineProperty(o, k2, desc);
|
|
29
|
+
}) : (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
o[k2] = m[k];
|
|
32
|
+
}));
|
|
33
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
34
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
35
|
+
}) : function(o, v) {
|
|
36
|
+
o["default"] = v;
|
|
37
|
+
});
|
|
38
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
39
|
+
var ownKeys = function(o) {
|
|
40
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
41
|
+
var ar = [];
|
|
42
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
43
|
+
return ar;
|
|
44
|
+
};
|
|
45
|
+
return ownKeys(o);
|
|
46
|
+
};
|
|
47
|
+
return function (mod) {
|
|
48
|
+
if (mod && mod.__esModule) return mod;
|
|
49
|
+
var result = {};
|
|
50
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
51
|
+
__setModuleDefault(result, mod);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
|
+
exports.CID_STANDARDS = void 0;
|
|
57
|
+
exports.detectTokenStandard = detectTokenStandard;
|
|
58
|
+
exports.normalizeCidStandard = normalizeCidStandard;
|
|
59
|
+
exports.resolveTokenStandard = resolveTokenStandard;
|
|
60
|
+
exports.dp1ChainFromCaip2 = dp1ChainFromCaip2;
|
|
61
|
+
exports.resolveDp1Chain = resolveDp1Chain;
|
|
62
|
+
exports.extractArtistName = extractArtistName;
|
|
63
|
+
exports.isUsableSourceUrl = isUsableSourceUrl;
|
|
64
|
+
exports.getBestMediaUrl = getBestMediaUrl;
|
|
65
|
+
exports.itemIdFor = itemIdFor;
|
|
66
|
+
exports.convertToDP1Item = convertToDP1Item;
|
|
67
|
+
exports.stillOf = stillOf;
|
|
68
|
+
const node_crypto_1 = require("node:crypto");
|
|
69
|
+
const dp1_js_1 = require("dp1-js");
|
|
70
|
+
const logger = __importStar(require("../logger"));
|
|
71
|
+
const ref_manifest_1 = require("./ref-manifest");
|
|
72
|
+
// playlist-builder.js is still CommonJS; require keeps interop simple, the
|
|
73
|
+
// same way find.ts and playlist-source.ts load it.
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
75
|
+
const { applyTimingToItemBuilder, buildDefaultDisplay } = require('./playlist-builder');
|
|
76
|
+
/**
|
|
77
|
+
* detectTokenStandard guesses a token standard from a chain name and an address.
|
|
78
|
+
*
|
|
79
|
+
* This is the last-resort fallback, not the answer. It sees no token, only a
|
|
80
|
+
* chain name and an address format, so it cannot tell ERC-721 from ERC-1155
|
|
81
|
+
* and answers `erc721` for every EVM contract. It is still needed in two
|
|
82
|
+
* places: a lookup by coordinate has to build some key before a provider has
|
|
83
|
+
* been asked anything, and a token row that arrived without a `standard`
|
|
84
|
+
* field has nothing better to offer.
|
|
85
|
+
*
|
|
86
|
+
* Prefer, in order: the provider row's own standard, a caller's assertion, and
|
|
87
|
+
* only then this. On-chain detection is deliberately not attempted here.
|
|
88
|
+
*
|
|
89
|
+
* @param chain - Blockchain network
|
|
90
|
+
* @param contractAddress - Contract address
|
|
91
|
+
* @returns Token standard (erc721, erc1155, fa2, or other)
|
|
92
|
+
*/
|
|
93
|
+
function detectTokenStandard(chain, contractAddress) {
|
|
94
|
+
const lowerChain = String(chain || '').toLowerCase();
|
|
95
|
+
// Tezos contracts use FA2 standard
|
|
96
|
+
if (lowerChain === 'tezos' || String(contractAddress || '').startsWith('KT')) {
|
|
97
|
+
return 'fa2';
|
|
98
|
+
}
|
|
99
|
+
// Ethereum: erc721 is a guess, not a reading. Only the provider row or a
|
|
100
|
+
// caller's assertion can distinguish an ERC-1155 token from an ERC-721 one.
|
|
101
|
+
if (lowerChain === 'ethereum') {
|
|
102
|
+
return 'erc721';
|
|
103
|
+
}
|
|
104
|
+
return 'other';
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* CID_STANDARDS are the token standards the indexer keys CIDs on and DP-1
|
|
108
|
+
* `provenance.contract.standard` accepts from this CLI. A standard outside
|
|
109
|
+
* this set, asserted by a caller or arriving on a row, says nothing the key
|
|
110
|
+
* can carry, so detection decides instead.
|
|
111
|
+
*/
|
|
112
|
+
exports.CID_STANDARDS = new Set(['erc721', 'erc1155', 'fa2']);
|
|
113
|
+
/**
|
|
114
|
+
* normalizeCidStandard returns a standard the CID format can carry, or ''.
|
|
115
|
+
*
|
|
116
|
+
* Both a provider row and a caller's assertion are untrusted strings from
|
|
117
|
+
* outside this module ('ERC1155', ' fa2 ', 'ERC-1155', undefined). Anything not
|
|
118
|
+
* in CID_STANDARDS is treated as absent rather than passed through, so a
|
|
119
|
+
* misspelled standard never reaches a CID or a DP-1 `provenance.contract`.
|
|
120
|
+
*
|
|
121
|
+
* @param value - Candidate standard
|
|
122
|
+
* @returns Normalized standard, or '' when it is not one we can use
|
|
123
|
+
*/
|
|
124
|
+
function normalizeCidStandard(value) {
|
|
125
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
126
|
+
return exports.CID_STANDARDS.has(normalized) ? normalized : '';
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* resolveTokenStandard picks the standard a lookup key is built with.
|
|
130
|
+
*
|
|
131
|
+
* detectTokenStandard cannot tell ERC-721 from ERC-1155, so when the caller
|
|
132
|
+
* knows the standard (a DP-1 playlist carries it in
|
|
133
|
+
* `provenance.contract.standard`) that assertion wins.
|
|
134
|
+
*
|
|
135
|
+
* @param chain - Blockchain network
|
|
136
|
+
* @param contractAddress - Contract address
|
|
137
|
+
* @param asserted - Standard the caller vouches for, if any
|
|
138
|
+
* @returns Token standard for the lookup key
|
|
139
|
+
*/
|
|
140
|
+
function resolveTokenStandard(chain, contractAddress, asserted) {
|
|
141
|
+
return normalizeCidStandard(asserted) || detectTokenStandard(chain, contractAddress);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* CAIP2_NAMESPACE_TO_DP1_CHAIN maps CAIP-2 namespaces to the chain values DP-1
|
|
145
|
+
* `provenance.contract` allows. Namespaces outside this map have no DP-1
|
|
146
|
+
* equivalent.
|
|
147
|
+
*/
|
|
148
|
+
const CAIP2_NAMESPACE_TO_DP1_CHAIN = {
|
|
149
|
+
eip155: 'evm',
|
|
150
|
+
tezos: 'tezos',
|
|
151
|
+
bitmark: 'bitmark',
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* dp1ChainFromCaip2 derives a DP-1 chain from a CAIP-2 chain id.
|
|
155
|
+
*
|
|
156
|
+
* @param caip2Chain - CAIP-2 chain id from a provider row, e.g. `eip155:1`
|
|
157
|
+
* @returns DP-1 chain (`evm`, `tezos`, `bitmark`), or '' when unknown or absent
|
|
158
|
+
*/
|
|
159
|
+
function dp1ChainFromCaip2(caip2Chain) {
|
|
160
|
+
if (typeof caip2Chain !== 'string') {
|
|
161
|
+
return '';
|
|
162
|
+
}
|
|
163
|
+
const namespace = caip2Chain.trim().toLowerCase().split(':')[0];
|
|
164
|
+
return CAIP2_NAMESPACE_TO_DP1_CHAIN[namespace] || '';
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* CHAIN_NAME_TO_DP1_CHAIN maps client chain names to the chain values DP-1
|
|
168
|
+
* `provenance.contract` allows. It answers only for tokens that reached us
|
|
169
|
+
* without a provider row, that is a caller-supplied coordinate.
|
|
170
|
+
*/
|
|
171
|
+
const CHAIN_NAME_TO_DP1_CHAIN = {
|
|
172
|
+
ethereum: 'evm',
|
|
173
|
+
polygon: 'evm',
|
|
174
|
+
arbitrum: 'evm',
|
|
175
|
+
optimism: 'evm',
|
|
176
|
+
base: 'evm',
|
|
177
|
+
zora: 'evm',
|
|
178
|
+
tezos: 'tezos',
|
|
179
|
+
bitmark: 'bitmark',
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* resolveDp1Chain picks the DP-1 chain for a token's `provenance.contract`.
|
|
183
|
+
*
|
|
184
|
+
* The row's CAIP-2 chain and the caller's chain name are not two guesses to try
|
|
185
|
+
* in turn: they are different claims, and the row's is the network the
|
|
186
|
+
* provider recorded. So a token carrying a row chain is answered from that
|
|
187
|
+
* chain alone; a namespace with no DP-1 equivalent becomes `other`, never the
|
|
188
|
+
* caller's name. Falling through would be actively wrong, because owner and
|
|
189
|
+
* contract lookups label every non-KT address `ethereum` before mapping, so a
|
|
190
|
+
* Solana row would inherit that label and be published as an EVM contract.
|
|
191
|
+
*
|
|
192
|
+
* @param token - Token in the internal standard format
|
|
193
|
+
* @returns DP-1 chain (`evm`, `tezos`, `bitmark`, or `other`)
|
|
194
|
+
*/
|
|
195
|
+
function resolveDp1Chain(token) {
|
|
196
|
+
const rowChain = typeof token.caip2Chain === 'string' ? token.caip2Chain.trim() : '';
|
|
197
|
+
if (rowChain) {
|
|
198
|
+
return dp1ChainFromCaip2(rowChain) || 'other';
|
|
199
|
+
}
|
|
200
|
+
return CHAIN_NAME_TO_DP1_CHAIN[String(token.chain || '').toLowerCase()] || 'other';
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* extractArtistName returns the first artist's name from an artists array, or ''.
|
|
204
|
+
*
|
|
205
|
+
* @param artists - Array of artist objects with a name
|
|
206
|
+
* @returns Artist name or empty string
|
|
207
|
+
*/
|
|
208
|
+
function extractArtistName(artists) {
|
|
209
|
+
if (!Array.isArray(artists) || artists.length === 0) {
|
|
210
|
+
return '';
|
|
211
|
+
}
|
|
212
|
+
const first = artists[0];
|
|
213
|
+
return typeof first?.name === 'string' ? first.name : '';
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* isUsableSourceUrl reports whether a media URL can be a DP-1 item source.
|
|
217
|
+
*
|
|
218
|
+
* Data URIs are refused because the FF1 display path cannot load them and
|
|
219
|
+
* inlining base64 would dwarf the playlist; URLs over 1024 characters exceed
|
|
220
|
+
* the DP-1 limit on `source`.
|
|
221
|
+
*
|
|
222
|
+
* @param url - Candidate media URL
|
|
223
|
+
* @returns True when URL is usable in a DP-1 source
|
|
224
|
+
*/
|
|
225
|
+
function isUsableSourceUrl(url) {
|
|
226
|
+
if (!url || typeof url !== 'string') {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (url.startsWith('data:')) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (url.length > 1024) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* getBestMediaUrl picks a DP-1 usable URL from a `display` block and asset rows.
|
|
239
|
+
*
|
|
240
|
+
* Order: display.animation_url, transcoded asset URLs (source + variants),
|
|
241
|
+
* display.image_url. The thumbnail is always `display.image_url`, the field
|
|
242
|
+
* reserved for a still.
|
|
243
|
+
*
|
|
244
|
+
* @param display - Merged presentation block
|
|
245
|
+
* @param mediaAssets - Transcoded asset rows
|
|
246
|
+
* @returns Object with url and thumbnail properties
|
|
247
|
+
*/
|
|
248
|
+
function getBestMediaUrl(display = {}, mediaAssets = []) {
|
|
249
|
+
const urlsFromAssets = [];
|
|
250
|
+
for (const asset of Array.isArray(mediaAssets) ? mediaAssets : []) {
|
|
251
|
+
if (asset?.source_url) {
|
|
252
|
+
urlsFromAssets.push(asset.source_url);
|
|
253
|
+
}
|
|
254
|
+
const v = asset?.variants;
|
|
255
|
+
if (v && typeof v === 'object') {
|
|
256
|
+
for (const val of Object.values(v)) {
|
|
257
|
+
if (typeof val === 'string') {
|
|
258
|
+
urlsFromAssets.push(val);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const candidates = [display?.animation_url, ...urlsFromAssets, display?.image_url];
|
|
264
|
+
for (const candidate of candidates) {
|
|
265
|
+
if (isUsableSourceUrl(candidate)) {
|
|
266
|
+
return {
|
|
267
|
+
url: candidate,
|
|
268
|
+
thumbnail: display?.image_url || '',
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const imageUrl = display?.image_url || '';
|
|
273
|
+
return {
|
|
274
|
+
url: imageUrl,
|
|
275
|
+
thumbnail: imageUrl,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* itemIdFor derives the deterministic DP-1 item id for a token coordinate.
|
|
280
|
+
*
|
|
281
|
+
* sha256 of `${contractAddress}-${tokenId}` formatted UUID-style. This is a
|
|
282
|
+
* cross-vendor contract: a playlist built from the indexer and one built from
|
|
283
|
+
* Raster must carry the same id for the same token.
|
|
284
|
+
*
|
|
285
|
+
* @param token - Token coordinate
|
|
286
|
+
* @returns UUID-formatted id
|
|
287
|
+
*/
|
|
288
|
+
function itemIdFor(token) {
|
|
289
|
+
const idSource = `${token.contractAddress}-${token.tokenId}`;
|
|
290
|
+
const hash = (0, node_crypto_1.createHash)('sha256').update(idSource).digest('hex');
|
|
291
|
+
return `${hash.substr(0, 8)}-${hash.substr(8, 4)}-${hash.substr(12, 4)}-${hash.substr(16, 4)}-${hash.substr(20, 12)}`;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* pickSourceUrl chooses the item source from a token's media candidates.
|
|
295
|
+
*
|
|
296
|
+
* Priority: animation_url, then image.url. The first usable candidate wins;
|
|
297
|
+
* when none is usable the first non-empty one is returned so the caller can
|
|
298
|
+
* report precisely why it was rejected.
|
|
299
|
+
*
|
|
300
|
+
* @param token - Token in the internal standard format
|
|
301
|
+
* @returns Source URL, or '' when the token names no media at all
|
|
302
|
+
*/
|
|
303
|
+
function pickSourceUrl(token) {
|
|
304
|
+
const candidateSourceUrls = [
|
|
305
|
+
token.animation_url || token.animationUrl,
|
|
306
|
+
token.image && typeof token.image === 'object' ? token.image.url : '',
|
|
307
|
+
]
|
|
308
|
+
.map((value) => String(value || ''))
|
|
309
|
+
.filter(Boolean);
|
|
310
|
+
return candidateSourceUrls.find((url) => isUsableSourceUrl(url)) || candidateSourceUrls[0] || '';
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* rejectSourceUrl explains why a source URL cannot be used, or returns ''.
|
|
314
|
+
*
|
|
315
|
+
* @param token - Token the URL belongs to, for the log line
|
|
316
|
+
* @param sourceUrl - Candidate source
|
|
317
|
+
* @returns Error message, or '' when the URL is usable
|
|
318
|
+
*/
|
|
319
|
+
function rejectSourceUrl(token, sourceUrl) {
|
|
320
|
+
const coordinate = { contractAddress: token.contractAddress, tokenId: token.tokenId };
|
|
321
|
+
if (!sourceUrl) {
|
|
322
|
+
logger.warn('[Token Item] No source URL found for token:', coordinate);
|
|
323
|
+
return 'No source URL available';
|
|
324
|
+
}
|
|
325
|
+
if (sourceUrl.startsWith('data:')) {
|
|
326
|
+
logger.debug('[Token Item] Skipping token with data URI:', coordinate);
|
|
327
|
+
return 'Source is a data URI (not supported)';
|
|
328
|
+
}
|
|
329
|
+
if (sourceUrl.length > 1024) {
|
|
330
|
+
logger.debug('[Token Item] Skipping token with source URL too long:', {
|
|
331
|
+
...coordinate,
|
|
332
|
+
urlLength: sourceUrl.length,
|
|
333
|
+
});
|
|
334
|
+
return `Source URL too long (${sourceUrl.length} chars, max 1024)`;
|
|
335
|
+
}
|
|
336
|
+
return '';
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* convertToDP1Item converts a token in the standard format to a DP-1 item.
|
|
340
|
+
*
|
|
341
|
+
* When the token carries description, artist, or a still image distinct from
|
|
342
|
+
* the source, the item also gets an inline Ref Manifest (see ref-manifest.ts).
|
|
343
|
+
*
|
|
344
|
+
* @param tokenData - Token data in standard format
|
|
345
|
+
* @param duration - Explicit display seconds; omit for auto timing
|
|
346
|
+
* (video/audio play natural length per DP-1 §4.1)
|
|
347
|
+
* @returns DP-1 item with success/error status, plus the token's still
|
|
348
|
+
*/
|
|
349
|
+
function convertToDP1Item(tokenData, duration) {
|
|
350
|
+
const { token } = tokenData;
|
|
351
|
+
if (!token) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
error: tokenData.error || 'Invalid token data',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const sourceUrl = pickSourceUrl(token);
|
|
358
|
+
const rejection = rejectSourceUrl(token, sourceUrl);
|
|
359
|
+
if (rejection) {
|
|
360
|
+
return { success: false, error: rejection };
|
|
361
|
+
}
|
|
362
|
+
// Build via dp1-js PlaylistItemBuilder so leaf blocks match the DP-1 AJV schema.
|
|
363
|
+
const itemBuilder = new dp1_js_1.PlaylistItemBuilder()
|
|
364
|
+
.id(itemIdFor(token))
|
|
365
|
+
.source(sourceUrl)
|
|
366
|
+
.license('open')
|
|
367
|
+
.provenance(new dp1_js_1.ProvenanceBuilder().type('onChain').contract(new dp1_js_1.ContractBuilder()
|
|
368
|
+
// resolveDp1Chain and the standard helpers only ever return members
|
|
369
|
+
// of the dp1-js unions, so the casts narrow rather than lie.
|
|
370
|
+
.chain(resolveDp1Chain(token))
|
|
371
|
+
.standard((token.standard ||
|
|
372
|
+
detectTokenStandard(token.chain, token.contractAddress)))
|
|
373
|
+
.address(token.contractAddress)
|
|
374
|
+
.tokenId(String(token.tokenId))));
|
|
375
|
+
if (token.name) {
|
|
376
|
+
itemBuilder.title(token.name);
|
|
377
|
+
}
|
|
378
|
+
// Carry description, artist, and still image inline (DP-1 Playlist Extension
|
|
379
|
+
// §3.6). The helper returns undefined when the token has nothing beyond its
|
|
380
|
+
// title, so thin tokens stay thin.
|
|
381
|
+
const inlineManifest = (0, ref_manifest_1.buildInlineManifestForToken)(token, { sourceUrl });
|
|
382
|
+
if (inlineManifest) {
|
|
383
|
+
itemBuilder.inlineManifest(inlineManifest);
|
|
384
|
+
}
|
|
385
|
+
const mimeType = typeof token.image === 'object' && token.image ? token.image.mimeType : undefined;
|
|
386
|
+
applyTimingToItemBuilder(itemBuilder, { mimeType, sourceUrl }, duration, buildDefaultDisplay());
|
|
387
|
+
let dp1Item;
|
|
388
|
+
try {
|
|
389
|
+
dp1Item = itemBuilder.build();
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
error: error instanceof Error ? error.message : String(error),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
logger.debug('[Token Item] ✓ Converted to DP-1:', {
|
|
398
|
+
title: token.name,
|
|
399
|
+
source: sourceUrl ? sourceUrl.substring(0, 60) + '...' : '(no source URL)',
|
|
400
|
+
});
|
|
401
|
+
return {
|
|
402
|
+
success: true,
|
|
403
|
+
item: dp1Item,
|
|
404
|
+
still: stillOf(token),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* stillOf names the token's still image as the provider identifies it, or ''.
|
|
409
|
+
*
|
|
410
|
+
* Only `image.thumbnail` qualifies. `image.url` is the media the provider
|
|
411
|
+
* selected as the source, which is the live rendition whenever one exists,
|
|
412
|
+
* exactly what must never land in a thumbnail slot. It is returned beside the
|
|
413
|
+
* DP-1 item rather than inside it because the item only carries the still when
|
|
414
|
+
* it differs from the item's own source, and a consumer building a manifest
|
|
415
|
+
* against a different source (see enrich-playlist.ts) needs the still
|
|
416
|
+
* regardless. Nothing about the URL's shape is inspected: the provider's word
|
|
417
|
+
* that it is an image is the evidence, and a suffix check would reject the
|
|
418
|
+
* extensionless CDN URLs providers commonly return.
|
|
419
|
+
*
|
|
420
|
+
* @param token - Token in the internal standard format
|
|
421
|
+
* @returns http(s) still URL, or '' when the provider named none
|
|
422
|
+
*/
|
|
423
|
+
function stillOf(token) {
|
|
424
|
+
const image = token.image;
|
|
425
|
+
const candidate = typeof image === 'string' ? image : typeof image === 'object' && image ? image.thumbnail : '';
|
|
426
|
+
const trimmed = String(candidate || '').trim();
|
|
427
|
+
return trimmed.startsWith('http://') || trimmed.startsWith('https://') ? trimmed : '';
|
|
428
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Token source contract: the vendor-neutral seam between commands and the
|
|
4
|
+
* services that answer "which token is this, and what does it display".
|
|
5
|
+
*
|
|
6
|
+
* Commands (`find`, `build`, `enrich`) depend on this contract only. Each
|
|
7
|
+
* vendor is one provider module that implements it: token-source-indexer.ts
|
|
8
|
+
* (the default) and token-source-raster.ts (opt-in, feral-file#3576).
|
|
9
|
+
* A future art-catalog-service adapter would be a third provider, and nothing
|
|
10
|
+
* outside token-source.ts would change.
|
|
11
|
+
*
|
|
12
|
+
* Kept free of provider imports so providers can import it without a cycle;
|
|
13
|
+
* the factory that chooses a provider lives in token-source.ts.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.RASTER_CANNOT_LIST_CONTRACT = exports.TokenSourceCapabilityError = void 0;
|
|
17
|
+
exports.isResolved = isResolved;
|
|
18
|
+
exports.isFailed = isFailed;
|
|
19
|
+
exports.itemsOf = itemsOf;
|
|
20
|
+
exports.failureMessages = failureMessages;
|
|
21
|
+
/**
|
|
22
|
+
* isResolved narrows a result. This repo compiles without strictNullChecks,
|
|
23
|
+
* under which TypeScript does not narrow a union on a boolean discriminant,
|
|
24
|
+
* so callers use these guards instead of `if (result.success)`.
|
|
25
|
+
*/
|
|
26
|
+
function isResolved(result) {
|
|
27
|
+
return result.success === true;
|
|
28
|
+
}
|
|
29
|
+
/** isFailed is the complement of isResolved. */
|
|
30
|
+
function isFailed(result) {
|
|
31
|
+
return result.success === false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* TokenSourceCapabilityError means the active provider cannot answer this
|
|
35
|
+
* kind of request at all. It is deliberately not retried against another
|
|
36
|
+
* provider: one vendor answers a whole run (owner decision, feral-file#3576).
|
|
37
|
+
*/
|
|
38
|
+
class TokenSourceCapabilityError extends Error {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'TokenSourceCapabilityError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.TokenSourceCapabilityError = TokenSourceCapabilityError;
|
|
45
|
+
/** RASTER_CANNOT_LIST_CONTRACT is the user-facing reason contract listing fails on Raster. */
|
|
46
|
+
exports.RASTER_CANNOT_LIST_CONTRACT = 'Raster cannot list a contract. Give tokenIds, an owner address, or run find on the series URL.';
|
|
47
|
+
/** itemsOf keeps the items of the successful results, in order. */
|
|
48
|
+
function itemsOf(results) {
|
|
49
|
+
return results.filter(isResolved).map((result) => result.item);
|
|
50
|
+
}
|
|
51
|
+
/** failureMessages returns each distinct failure reason once, for a summary line. */
|
|
52
|
+
function failureMessages(results) {
|
|
53
|
+
const messages = results.filter(isFailed).map((result) => result.error);
|
|
54
|
+
return [...new Set(messages)];
|
|
55
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Indexer token source: adapter over the ff-indexer-v2 client.
|
|
4
|
+
*
|
|
5
|
+
* The indexer is the default token source. Raster is the opt-in alternative
|
|
6
|
+
* under evaluation (feral-file#3576); keep both behind the TokenSource
|
|
7
|
+
* contract so the vendor stays a configuration switch. This adapter keeps the
|
|
8
|
+
* pre-Raster behaviour on purpose, including the indexing trigger and the
|
|
9
|
+
* polling that makes unseen tokens slow (ff-cli#132).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createIndexerTokenSource = createIndexerTokenSource;
|
|
13
|
+
// nft-indexer.js is CommonJS and is required lazily so a Raster run never
|
|
14
|
+
// loads the indexer client at all.
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
|
+
const loadIndexer = () => require('./nft-indexer');
|
|
17
|
+
/** Page size the owner walk has always used against the indexer. */
|
|
18
|
+
const OWNER_PAGE_SIZE = 50;
|
|
19
|
+
/** Per-request cap the indexer path has always applied to a bounded listing. */
|
|
20
|
+
const MAX_BOUNDED_LIMIT = 100;
|
|
21
|
+
/** chainOfContract infers a chain from an address; listings carry no chain name. */
|
|
22
|
+
function chainOfContract(contractAddress) {
|
|
23
|
+
return contractAddress.startsWith('KT') ? 'tezos' : 'ethereum';
|
|
24
|
+
}
|
|
25
|
+
/** rowsToResults converts raw indexer rows to results, one per row. */
|
|
26
|
+
function rowsToResults(rows, duration) {
|
|
27
|
+
const indexer = loadIndexer();
|
|
28
|
+
return rows.map((row) => {
|
|
29
|
+
const contractAddress = row.contract_address || '';
|
|
30
|
+
const coordinate = {
|
|
31
|
+
chain: chainOfContract(contractAddress),
|
|
32
|
+
contractAddress,
|
|
33
|
+
tokenId: String(row.token_number ?? ''),
|
|
34
|
+
};
|
|
35
|
+
const mapped = indexer.mapIndexerDataToStandardFormat(row, coordinate.chain);
|
|
36
|
+
const converted = mapped.success ? indexer.convertToDP1Item(mapped, duration) : mapped;
|
|
37
|
+
if (!converted.success || !converted.item) {
|
|
38
|
+
return { success: false, coordinate, error: converted.error || 'Item build failed' };
|
|
39
|
+
}
|
|
40
|
+
return { success: true, coordinate, item: converted.item, still: converted.still || '' };
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* walkOwner pages through the indexer for an owner, as queryTokensByAddress did.
|
|
45
|
+
* A failed first page throws; a failed later page keeps what was collected,
|
|
46
|
+
* which is the indexer path's long-standing behaviour.
|
|
47
|
+
*/
|
|
48
|
+
async function walkOwner(address, limit) {
|
|
49
|
+
const indexer = loadIndexer();
|
|
50
|
+
if (limit !== 'all') {
|
|
51
|
+
const result = await indexer.queryTokensByOwner(address, Math.min(limit, MAX_BOUNDED_LIMIT));
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
throw new Error(result.error || 'Indexer owner query failed');
|
|
54
|
+
}
|
|
55
|
+
return result.tokens;
|
|
56
|
+
}
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (let offset = 0;; offset += OWNER_PAGE_SIZE) {
|
|
59
|
+
const result = await indexer.queryTokensByOwner(address, OWNER_PAGE_SIZE, offset);
|
|
60
|
+
if (!result.success && offset === 0) {
|
|
61
|
+
throw new Error(result.error || 'Indexer owner query failed');
|
|
62
|
+
}
|
|
63
|
+
if (!result.success || result.tokens.length === 0) {
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
rows.push(...result.tokens);
|
|
67
|
+
if (result.tokens.length < OWNER_PAGE_SIZE) {
|
|
68
|
+
return rows;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** createIndexerTokenSource builds the indexer provider. */
|
|
73
|
+
function createIndexerTokenSource() {
|
|
74
|
+
return {
|
|
75
|
+
name: 'indexer',
|
|
76
|
+
async resolveTokens(coords, opts = {}) {
|
|
77
|
+
const raw = await loadIndexer().resolveTokenBatch(coords, opts.duration, opts.onProgress);
|
|
78
|
+
return coords.map((coordinate, i) => {
|
|
79
|
+
const result = raw[i];
|
|
80
|
+
if (result?.success && result.item) {
|
|
81
|
+
return { success: true, coordinate, item: result.item, still: result.still || '' };
|
|
82
|
+
}
|
|
83
|
+
return { success: false, coordinate, error: result?.error || 'Token not resolved' };
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
async listTokensByOwner(address, opts) {
|
|
87
|
+
const rows = await walkOwner(address, opts.limit);
|
|
88
|
+
return { results: rowsToResults(rows, opts.duration), skippedUnsupported: 0 };
|
|
89
|
+
},
|
|
90
|
+
async listTokensByContract(address, opts) {
|
|
91
|
+
const limit = opts.limit === 'all' ? MAX_BOUNDED_LIMIT : Math.min(opts.limit, MAX_BOUNDED_LIMIT);
|
|
92
|
+
const result = await loadIndexer().queryTokensByContract(address, limit);
|
|
93
|
+
if (!result.success) {
|
|
94
|
+
throw new Error(result.error || 'Indexer contract query failed');
|
|
95
|
+
}
|
|
96
|
+
return { results: rowsToResults(result.tokens, opts.duration), skippedUnsupported: 0 };
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|