@feralfile/cli 2.6.0 → 2.8.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.
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /**
2
3
  * NFT Indexer Client
3
4
  * This module provides functions to interact with NFT indexing services
@@ -48,8 +49,15 @@ const JOB_STATUS_QUERY = `
48
49
  /**
49
50
  * buildTokensListQuery builds the `tokens { items { ... } }` request body used by queryTokens.
50
51
  *
51
- * Selection uses only `display` and `media_assets`. The indexer merges metadata and enrichment
52
- * into `display`; the client does not repeat that merge by fetching raw metadata fields.
52
+ * Presentation comes only from `display` and `media_assets`. The indexer merges metadata and
53
+ * enrichment into `display`; the client does not repeat that merge by fetching raw metadata fields.
54
+ *
55
+ * `chain` and `standard` are selected because the row is the only place either fact is known.
56
+ * `detectTokenStandard` has an address and a chain name and nothing else, so it answers `erc721` for
57
+ * every EVM contract — which wrote `erc721` into playlists for every ERC-1155 token `find` reached by
58
+ * owner or by contract. The indexer stores what the token actually is (`erc721`, `erc1155`, `fa2`)
59
+ * beside its CAIP-2 `chain` (`eip155:1`, `tezos:mainnet`), so the row answers both questions and
60
+ * detection drops to a fallback. Do not remove these two fields from the selection.
53
61
  *
54
62
  * @param {Object} [params]
55
63
  * @param {Array<string>} [params.token_cids]
@@ -74,6 +82,8 @@ function buildTokensListQuery(params = {}) {
74
82
  token_number
75
83
  current_owner
76
84
  burned
85
+ chain
86
+ standard
77
87
  display {
78
88
  name
79
89
  description
@@ -106,10 +116,18 @@ function initializeIndexer(_config) {
106
116
  logger.debug('[NFT Indexer] Using endpoint:', GRAPHQL_ENDPOINT);
107
117
  }
108
118
  /**
109
- * Detect token standard based on chain and contract address
119
+ * detectTokenStandard guesses a token standard from a chain name and an address.
120
+ *
121
+ * This is the last-resort fallback, not the answer. It sees no token — only a
122
+ * chain name and an address format — so it cannot tell ERC-721 from ERC-1155
123
+ * and answers `erc721` for every EVM contract. It is still needed in two
124
+ * places: a lookup by coordinate has to build *some* CID before the indexer has
125
+ * been asked anything (see buildTokenCID), and a token row that arrived without
126
+ * a `standard` field has nothing better to offer.
110
127
  *
111
- * Determines the appropriate ERC/token standard for the given blockchain
112
- * and contract format.
128
+ * Prefer, in order: the indexer row's own `standard`, a caller's assertion, and
129
+ * only then this. On-chain detection is deliberately not attempted here — the
130
+ * indexer already stores the answer and selecting it costs nothing.
113
131
  *
114
132
  * @param {string} chain - Blockchain network
115
133
  * @param {string} contractAddress - Contract address
@@ -121,18 +139,34 @@ function detectTokenStandard(chain, contractAddress) {
121
139
  if (lowerChain === 'tezos' || contractAddress.startsWith('KT')) {
122
140
  return 'fa2';
123
141
  }
124
- // Ethereum uses ERC721
125
- // TODO: Enhance with on-chain detection for ERC1155 support
142
+ // Ethereum: erc721 is a guess, not a reading. Only the indexer row or a
143
+ // caller's assertion can distinguish an ERC-1155 token from an ERC-721 one.
126
144
  if (lowerChain === 'ethereum') {
127
145
  return 'erc721';
128
146
  }
129
147
  return 'other';
130
148
  }
131
149
  /**
132
- * Token standards the indexer keys CIDs on. An asserted standard outside this
133
- * set says nothing the CID can carry, so detection decides instead.
150
+ * Token standards the indexer keys CIDs on. A standard outside this set —
151
+ * asserted by a caller or arriving on a row — says nothing the CID can carry,
152
+ * so detection decides instead.
134
153
  */
135
154
  const CID_STANDARDS = new Set(['erc721', 'erc1155', 'fa2']);
155
+ /**
156
+ * normalizeCidStandard returns a standard the CID format can carry, or ''.
157
+ *
158
+ * Both the indexer row and a caller's assertion are untrusted strings from
159
+ * outside this module ('ERC1155', ' fa2 ', 'ERC-1155', undefined). Anything not
160
+ * in CID_STANDARDS is treated as absent rather than passed through, so a
161
+ * misspelled standard never reaches a CID or a DP-1 `provenance.contract`.
162
+ *
163
+ * @param {unknown} value - Candidate standard
164
+ * @returns {string} Normalized standard, or '' when it is not one we can use
165
+ */
166
+ function normalizeCidStandard(value) {
167
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
168
+ return CID_STANDARDS.has(normalized) ? normalized : '';
169
+ }
136
170
  /**
137
171
  * resolveTokenStandard picks the standard a CID is built with.
138
172
  *
@@ -149,11 +183,77 @@ const CID_STANDARDS = new Set(['erc721', 'erc1155', 'fa2']);
149
183
  * @returns {string} Token standard for the CID
150
184
  */
151
185
  function resolveTokenStandard(chain, contractAddress, asserted) {
152
- const normalized = typeof asserted === 'string' ? asserted.trim().toLowerCase() : '';
153
- if (CID_STANDARDS.has(normalized)) {
154
- return normalized;
186
+ return normalizeCidStandard(asserted) || detectTokenStandard(chain, contractAddress);
187
+ }
188
+ /**
189
+ * CAIP-2 namespaces mapped to the chain values DP-1 `provenance.contract` allows.
190
+ *
191
+ * The indexer names a token's chain in CAIP-2 (`eip155:1`, `tezos:mainnet`),
192
+ * which is authoritative: it comes from the row rather than from the caller's
193
+ * guess at which network an address belongs to. Namespaces outside this map
194
+ * have no DP-1 equivalent, so they fall back to the caller's chain name.
195
+ */
196
+ const CAIP2_NAMESPACE_TO_DP1_CHAIN = {
197
+ eip155: 'evm',
198
+ tezos: 'tezos',
199
+ bitmark: 'bitmark',
200
+ };
201
+ /**
202
+ * dp1ChainFromCaip2 derives a DP-1 chain from a CAIP-2 chain id.
203
+ *
204
+ * @param {unknown} caip2Chain - CAIP-2 chain id from an indexer row, e.g. `eip155:1`
205
+ * @returns {string} DP-1 chain (`evm`, `tezos`, `bitmark`), or '' when unknown or absent
206
+ */
207
+ function dp1ChainFromCaip2(caip2Chain) {
208
+ if (typeof caip2Chain !== 'string') {
209
+ return '';
155
210
  }
156
- return detectTokenStandard(chain, contractAddress);
211
+ const namespace = caip2Chain.trim().toLowerCase().split(':')[0];
212
+ return CAIP2_NAMESPACE_TO_DP1_CHAIN[namespace] || '';
213
+ }
214
+ /**
215
+ * Client chain names mapped to the chain values DP-1 `provenance.contract` allows.
216
+ *
217
+ * This is for DP-1 provenance output, NOT for indexer queries: the indexer uses
218
+ * `eth`/`tez`/`bmk` while DP-1 uses `evm`/`tezos`/`bitmark`. It answers only for
219
+ * tokens that reached us without an indexer row — a caller-supplied coordinate.
220
+ */
221
+ const CHAIN_NAME_TO_DP1_CHAIN = {
222
+ ethereum: 'evm',
223
+ polygon: 'evm',
224
+ arbitrum: 'evm',
225
+ optimism: 'evm',
226
+ base: 'evm',
227
+ zora: 'evm',
228
+ tezos: 'tezos', // DP1 spec uses 'tezos', not 'tez'
229
+ bitmark: 'bitmark', // DP1 spec uses 'bitmark', not 'bmk'
230
+ };
231
+ /**
232
+ * resolveDp1Chain picks the DP-1 chain for a token's `provenance.contract`.
233
+ *
234
+ * The row's CAIP-2 chain and the caller's chain name are not two guesses to try
235
+ * in turn — they are different claims, and the row's is the network the indexer
236
+ * recorded. So a token carrying a row chain is answered from that chain alone:
237
+ * a namespace with no DP-1 equivalent becomes `other`, never the caller's name.
238
+ * Falling through would be actively wrong, because the owner and contract paths
239
+ * label every non-KT address `ethereum` before mapping — a Solana row would
240
+ * inherit that label and be published as an EVM contract. `other` is a true
241
+ * statement about a chain DP-1 cannot name; `evm` is a false one.
242
+ *
243
+ * The name map answers only when the token has no row chain at all, which is a
244
+ * token assembled from a caller-supplied coordinate rather than an indexer row.
245
+ *
246
+ * @param {Object} token - Token in the internal standard format
247
+ * @param {string} [token.caip2Chain] - CAIP-2 chain from the indexer row, when it came from one
248
+ * @param {string} [token.chain] - Client chain name the caller inferred
249
+ * @returns {string} DP-1 chain (`evm`, `tezos`, `bitmark`, or `other`)
250
+ */
251
+ function resolveDp1Chain(token) {
252
+ const rowChain = typeof token.caip2Chain === 'string' ? token.caip2Chain.trim() : '';
253
+ if (rowChain) {
254
+ return dp1ChainFromCaip2(rowChain) || 'other';
255
+ }
256
+ return CHAIN_NAME_TO_DP1_CHAIN[String(token.chain || '').toLowerCase()] || 'other';
157
257
  }
158
258
  /**
159
259
  * Build CAIP-2 token CID for indexer v2
@@ -348,9 +448,16 @@ function getBestMediaUrl(display = {}, mediaAssets = []) {
348
448
  * is null or partial, we only use what is present (for example `Token #${token_number}` for name);
349
449
  * we do not splice in raw `metadata` or enrichment because those are not selected from GraphQL.
350
450
  *
451
+ * The row's own `standard` and `chain` outrank both the asserted `standard` and the `chain`
452
+ * argument, because they are the token's identity as the indexer recorded it while the arguments are
453
+ * the caller's inference — `find` by owner or by contract only knows "the address starts with 0x, so
454
+ * ethereum". Precedence for the standard is row → asserted → detection; the CAIP-2 `chain` is kept
455
+ * beside the caller's chain name (not in place of it) as `caip2Chain`, because the name still selects
456
+ * the CID prefix and the detection fallback while the CAIP-2 value answers DP-1's chain enum.
457
+ *
351
458
  * @param {Object} indexerData - Token fields from indexer GraphQL
352
- * @param {string} chain - Blockchain network
353
- * @param {string} [standard] - Asserted token standard; detected when omitted
459
+ * @param {string} chain - Blockchain network the caller inferred
460
+ * @param {string} [standard] - Asserted token standard; used when the row carries none
354
461
  * @returns {Object} Standardized token data
355
462
  */
356
463
  function mapIndexerDataToStandardFormat(indexerData, chain, standard) {
@@ -366,11 +473,14 @@ function mapIndexerDataToStandardFormat(indexerData, chain, standard) {
366
473
  const artistName = extractArtistName(display.artists);
367
474
  const name = display.name || `Token #${indexerData.token_number}`;
368
475
  const description = display.description || '';
369
- const resolvedStandard = resolveTokenStandard(chain, indexerData.contract_address, standard);
476
+ const resolvedStandard = normalizeCidStandard(indexerData.standard) ||
477
+ resolveTokenStandard(chain, indexerData.contract_address, standard);
478
+ const caip2Chain = typeof indexerData.chain === 'string' ? indexerData.chain : '';
370
479
  return {
371
480
  success: true,
372
481
  token: {
373
482
  chain,
483
+ caip2Chain,
374
484
  contractAddress: indexerData.contract_address,
375
485
  tokenId: indexerData.token_number,
376
486
  standard: resolvedStandard,
@@ -470,26 +580,13 @@ function convertToDP1Item(tokenData, duration) {
470
580
  error: `Source URL too long (${sourceUrl.length} chars, max 1024)`,
471
581
  };
472
582
  }
473
- // Map chain name to DP1 format (according to DP1 spec)
474
- // NOTE: This is for DP1 provenance output, NOT for indexer queries
475
- // The indexer uses 'eth'/'tez'/'bmk', but DP1 spec uses 'evm'/'tezos'/'bitmark'
476
- const chainMap = {
477
- ethereum: 'evm',
478
- polygon: 'evm',
479
- arbitrum: 'evm',
480
- optimism: 'evm',
481
- base: 'evm',
482
- zora: 'evm',
483
- tezos: 'tezos', // DP1 spec uses 'tezos', not 'tez'
484
- bitmark: 'bitmark', // DP1 spec uses 'bitmark', not 'bmk'
485
- };
486
583
  // Build via dp1-js PlaylistItemBuilder so leaf blocks match DP-1 AJV schema.
487
584
  const itemBuilder = new PlaylistItemBuilder()
488
585
  .id(itemId)
489
586
  .source(sourceUrl)
490
587
  .license('open')
491
588
  .provenance(new ProvenanceBuilder().type('onChain').contract(new ContractBuilder()
492
- .chain(chainMap[token.chain.toLowerCase()] || 'other')
589
+ .chain(resolveDp1Chain(token))
493
590
  .standard(token.standard || detectTokenStandard(token.chain, token.contractAddress))
494
591
  .address(token.contractAddress)
495
592
  .tokenId(String(token.tokenId))));
@@ -1,3 +1,4 @@
1
+ "use strict";
1
2
  /**
2
3
  * Playlist Builder Utilities
3
4
  *
@@ -1,37 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.castPlaylist = castPlaylist;
37
4
  /**
@@ -106,7 +73,7 @@ async function castPlaylist(playlist, options = {}) {
106
73
  };
107
74
  }
108
75
  }
109
- const { verifyPlaylist } = await Promise.resolve().then(() => __importStar(require('./playlist-verifier')));
76
+ const { verifyPlaylist } = await import('./playlist-verifier.js');
110
77
  const verifyResult = await verifyPlaylist(current);
111
78
  if (!verifyResult.valid) {
112
79
  return {