@manifoldxyz/marketplace-sdk 4.0.2 → 4.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/README.md CHANGED
@@ -1,7 +1,171 @@
1
- # marketplace-sdk
1
+ # @manifoldxyz/marketplace-sdk
2
2
 
3
- This package contains the core library for interacting with the Manifold marketplace.
3
+ TypeScript SDK for interacting with Manifold's on-chain marketplace contracts. Provides typed classes for managing listings (auctions, fixed-price, dynamic-price, offers-only), bids, offers, purchases, and real-time event subscriptions — used by Manifold's marketplace widgets and studio apps.
4
4
 
5
- The marketplace-sdk relies on the Manifold's EthereumProvider (node connection management library)
5
+ ## Prerequisites
6
6
 
7
- Please see code-comments for a full description of methods.
7
+ - **Node.js** >= 14
8
+ - **Yarn** (package manager)
9
+ - **ManifoldEthereumProvider** — must be available on `window.ManifoldEthereumProvider` at runtime (provided by Manifold's frontend infrastructure)
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ # Clone
15
+ git clone git@github.com:manifoldxyz/marketplace-sdk.git
16
+ cd marketplace-sdk
17
+
18
+ # Install dependencies
19
+ yarn install
20
+
21
+ # Build
22
+ yarn build
23
+
24
+ # Run tests
25
+ yarn test
26
+
27
+ # Lint
28
+ yarn lint
29
+ yarn lint:fix
30
+
31
+ # Format
32
+ yarn format
33
+ ```
34
+
35
+ ## Installation (as dependency)
36
+
37
+ ```bash
38
+ yarn add @manifoldxyz/marketplace-sdk
39
+ # or
40
+ npm install @manifoldxyz/marketplace-sdk
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```typescript
46
+ import { MarketplaceListing, MarketplaceTopic, ListingType } from '@manifoldxyz/marketplace-sdk';
47
+
48
+ // Create a listing instance
49
+ const listing = new MarketplaceListing(
50
+ marketplaceContractAddress,
51
+ networkId,
52
+ listingId,
53
+ 2 // version (default: 2)
54
+ );
55
+
56
+ // Wait for listing data to resolve
57
+ await listing.listingPromise;
58
+
59
+ // Subscribe to events
60
+ listing.subscribe(MarketplaceTopic.PURCHASE_EVENT_V2, (event) => {
61
+ console.log('Purchase event:', event);
62
+ });
63
+
64
+ // Clean up
65
+ listing.unsubscribeAll();
66
+ ```
67
+
68
+ ## Available Scripts
69
+
70
+ | Script | Command | Description |
71
+ |--------|---------|-------------|
72
+ | `build` | `yarn build` | Compiles TypeScript to `dist/` (cleans first) |
73
+ | `test` | `yarn test` | Runs TypeScript compiler + Jest test suite |
74
+ | `lint` | `yarn lint` | ESLint check |
75
+ | `lint:fix` | `yarn lint:fix` | ESLint with auto-fix |
76
+ | `format` | `yarn format` | Prettier + ESLint fix |
77
+
78
+ ## Architecture
79
+
80
+ ```
81
+ src/
82
+ ├── index.ts # Package entry — re-exports all public types
83
+ ├── lib/
84
+ │ ├── MarketplaceListing.ts # Core class — listing lifecycle, event subscriptions,
85
+ │ │ # bidding, purchasing, offers, identity verification
86
+ │ └── Web3Protocol.ts # Web3 call/write helpers with ManifoldBridgeProvider fallback
87
+ ├── classes/
88
+ │ ├── Listing.ts # Listing type hierarchy (Auction, FixedPrice, LazyFixed,
89
+ │ │ # DynamicPrice, OffersOnly + base abstract classes)
90
+ │ ├── ListingType.ts # Enum: AUCTION, FIXED_PRICE, DYNAMIC_PRICE, OFFERS_ONLY
91
+ │ ├── Bid.ts # Bid and BidHistory data classes
92
+ │ ├── Offer.ts # Offer and OfferHistory data classes
93
+ │ ├── Purchase.ts # Purchase data class
94
+ │ └── Token.ts # Token metadata class (address, media, attributes)
95
+ ├── abi/ # Contract ABIs
96
+ │ ├── MarketplaceContractABIv1.json
97
+ │ ├── MarketplaceContractABIv2.json
98
+ │ ├── IdentityVerifierABIv1.json
99
+ │ ├── IdentityVerifierABIv2.json
100
+ │ ├── IdentityVerifierViewABIv2.json
101
+ │ └── LazyDeliveryABI.json
102
+ └── ts-shims/
103
+ └── shims-manifold-window.d.ts # Window type augmentation for ManifoldEthereumProvider
104
+ ```
105
+
106
+ ### Key Concepts
107
+
108
+ - **MarketplaceListing** — Main entry point. Wraps a marketplace contract listing with methods for bidding, purchasing, making offers, fetching history, and subscribing to on-chain events. Supports contract versions 1 and 2.
109
+ - **Web3Protocol** — Provides `callWeb3()` and `writeWeb3()` with automatic fallback to Manifold's bridge provider when the browser provider is unavailable or times out.
110
+ - **Listing hierarchy** — Abstract `Listing` → `TokenListing` (concrete token) / `LazyTokenListing` (lazy-minted) → specific types (Auction, FixedPrice, etc.)
111
+ - **MarketplaceTopic** — Event topic hashes for on-chain event subscriptions (purchases, bids, offers).
112
+
113
+ ### API Endpoints (hardcoded)
114
+
115
+ | Network | Endpoint |
116
+ |---------|----------|
117
+ | Mainnet | `https://marketplace.api.manifoldxyz.dev` |
118
+ | Optimism | `https://optimism.marketplace.api.manifoldxyz.dev` |
119
+ | Base | `https://base.marketplace.api.manifoldxyz.dev` |
120
+ | Sepolia | `https://sepolia.marketplace.api.manifoldxyz.dev` |
121
+ | Shape | `https://shape.marketplace.api.manifoldxyz.dev` |
122
+ | Apechain | `https://apechain.marketplace.api.manifoldxyz.dev` |
123
+ | Campaign (Redeem) | `https://redeem.api.manifoldxyz.dev` |
124
+
125
+ ## Exported API
126
+
127
+ | Export | Type | Description |
128
+ |--------|------|-------------|
129
+ | `MarketplaceListing` | Class | Core listing manager — lifecycle, events, transactions |
130
+ | `MarketplaceTopic` | Enum | On-chain event topic hashes |
131
+ | `Listing` | Abstract Class | Base listing type |
132
+ | `ListingType` | Enum | `AUCTION`, `FIXED_PRICE`, `DYNAMIC_PRICE`, `OFFERS_ONLY` |
133
+ | `Token` | Class | Token metadata container |
134
+ | `Bid` / `BidHistory` | Class | Bid data types |
135
+ | `Offer` / `OfferHistory` | Class | Offer data types |
136
+
137
+ ## External Dependencies
138
+
139
+ ### Internal Packages
140
+
141
+ | Package | Version | Purpose |
142
+ |---------|---------|---------|
143
+ | `@manifoldxyz/js-ts-utils` | `^7.5.0` | Shared utilities (Media, Network types) |
144
+ | `@manifoldxyz/contract-abis` | `^1.0.0` | ERC20, ERC721, ERC1155 ABIs |
145
+ | `@manifoldxyz/ens-batch-lookup-ethers` | `^0.0.2` | Batch ENS name resolution |
146
+ | `@manifoldxyz/frontend-provider-types` | `^2.1.0` | Provider event types (CHAIN_CHANGED) |
147
+ | `@manifoldxyz/manifold-provider-client` | `^0.2.0` | ManifoldBridgeProvider for fallback web3 calls |
148
+ | `@manifoldxyz/lint-configs` | `^0.1.0` | Shared ESLint/Prettier configs (dev) |
149
+
150
+ ### Internal Services
151
+
152
+ | Service | Endpoint | Repo |
153
+ |---------|----------|------|
154
+ | Marketplace API | `marketplace.api.manifoldxyz.dev` | [marketplace-server](https://github.com/manifoldxyz/marketplace-server) |
155
+ | Redeem API | `redeem.api.manifoldxyz.dev` | — |
156
+
157
+ ### Third-Party Libraries
158
+
159
+ | Library | Purpose | Config |
160
+ |---------|---------|--------|
161
+ | `@ethersproject/*` | Ethereum contract interaction (BigNumber, units, ABI encoding) | — |
162
+ | `merkletreejs` + `keccak256` | Merkle tree construction for allowlist verification | — |
163
+
164
+ ## Publishing
165
+
166
+ This package is published as `@manifoldxyz/marketplace-sdk` to npm. The `files` field in `package.json` includes only `dist/` and `README.md`.
167
+
168
+ ```bash
169
+ yarn build
170
+ npm publish
171
+ ```
@@ -29,4 +29,18 @@ declare class Token implements IToken {
29
29
  burnt?: boolean | undefined;
30
30
  constructor(address: string, id: string, lazy: boolean, image_url: string, original_image_url: string | undefined, animation_url: string | undefined, original_animation_url: string | undefined, name: string, description: string, attributes: Array<object> | undefined, tokenData: JSON | undefined, originalURI: string, burnt?: boolean);
31
31
  }
32
+ /**
33
+ * The marketplace API's cached, CDN-optimized media for a token
34
+ * (`GET /token/<address>/<id>/media`). A URL is usable only when its matching
35
+ * `*_status` is `'ok'`.
36
+ */
37
+ interface TokenMedia {
38
+ image_status?: string;
39
+ image_url?: string;
40
+ original_image_url?: string;
41
+ animation_status?: string;
42
+ animation_url?: string;
43
+ original_animation_url?: string;
44
+ }
45
+ export type { TokenMedia };
32
46
  export default Token;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ import { Bid, BidHistory } from './classes/Bid';
2
2
  import { Listing } from './classes/Listing';
3
3
  import ListingType from './classes/ListingType';
4
4
  import { Offer, OfferHistory } from './classes/Offer';
5
- import Token from './classes/Token';
5
+ import Token, { TokenMedia } from './classes/Token';
6
6
  import { MarketplaceListing, MarketplaceTopic } from './lib/MarketplaceListing';
7
- export { MarketplaceTopic, MarketplaceListing, Listing, ListingType, Token, Bid, BidHistory, Offer, OfferHistory, };
7
+ export { MarketplaceTopic, MarketplaceListing, Listing, ListingType, Token, TokenMedia, Bid, BidHistory, Offer, OfferHistory, };
@@ -7,7 +7,7 @@ import { Bid, BidHistory } from '../classes/Bid';
7
7
  import { Listing } from '../classes/Listing';
8
8
  import { OfferHistory } from '../classes/Offer';
9
9
  import { Purchase } from '../classes/Purchase';
10
- import Token from '../classes/Token';
10
+ import Token, { TokenMedia } from '../classes/Token';
11
11
  declare enum MarketplaceTopic {
12
12
  PURCHASE_EVENT_V1 = "0xdc84c3c75a393011959e6391215e5b01982de6ad00f83b8b26ac7c52a5844b22",
13
13
  BID_EVENT_V1 = "0x68debf84f0c6c4d7f73019f874d0f4f4b4c6d895ebce90dc010abac51d74d60a",
@@ -174,9 +174,79 @@ declare class MarketplaceListing {
174
174
  */
175
175
  private _checkReferrer;
176
176
  /**
177
- * Get token details for this listing
177
+ * Get token details for this listing: on-chain metadata with the cached,
178
+ * CDN-optimized media applied on top.
179
+ *
180
+ * Composed of the independent reads below, fetched in parallel. A caller
181
+ * that renders text and media separately should use those directly:
182
+ * `getCachedTokenMetadata()` (fast, from the marketplace API's index) then
183
+ * `getTokenMetadata()` (authoritative, from the chain) for text, and
184
+ * `getTokenMedia()` for the artwork. None of them waits for another.
178
185
  */
179
186
  getTokenDetails(): Promise<Token>;
187
+ /**
188
+ * The token's own metadata: name, description, attributes and the media URLs
189
+ * the metadata declares, read via tokenURI (`uri` for ERC1155, `assetURI` for
190
+ * lazy delivery). Does NOT wait for, or apply, the cached media -- see
191
+ * `getTokenMedia()` and `applyTokenMedia()`.
192
+ *
193
+ * Burnt tokens (tokenURI reverts) fall back to the marketplace API's cached
194
+ * copy, which already carries cached media URLs.
195
+ */
196
+ getTokenMetadata(): Promise<Token>;
197
+ /**
198
+ * The marketplace API's cached, CDN-optimized media for this listing's token
199
+ * (`/token/<address>/<id>/media`). Needs only the token address and id, so it
200
+ * does not wait for tokenURI or the metadata.
201
+ *
202
+ * Never rejects: resolves to `null` when the lookup fails or has no entry.
203
+ * Shares one request with `getCachedTokenMetadata()` (see
204
+ * `_fetchSharedCachedToken`).
205
+ */
206
+ getTokenMedia(): Promise<TokenMedia | null>;
207
+ /**
208
+ * The token's name, description and attributes as last indexed by the
209
+ * marketplace API (NFT proxy), WITHOUT reading the chain. Answers in ~0.1s;
210
+ * `getTokenMetadata()` needs a tokenURI RPC plus the metadata fetch, and on
211
+ * contracts that build metadata on-chain the RPC can take seconds or fail.
212
+ *
213
+ * For showing the token's text immediately. It is an index, not the source of
214
+ * truth: callers must replace it with `getTokenMetadata()` when that lands.
215
+ * Measured on 60 live mainnet listings: indexed for 39, and for all 39 the
216
+ * name and description equalled the chain's.
217
+ *
218
+ * Media URLs are the metadata's own, as in `getTokenMetadata()`; cached media
219
+ * comes from `getTokenMedia()`. Never rejects: `null` when the token is not
220
+ * indexed or the lookup fails.
221
+ */
222
+ getCachedTokenMetadata(): Promise<Token | null>;
223
+ /**
224
+ * The cached-token lookup WITH token details, shared by `getTokenMedia()`,
225
+ * `getCachedTokenMetadata()` and the burnt-token fallback in
226
+ * `getTokenMetadata()`: one `/media?withTokenDetails=true` request per load
227
+ * instead of up to three. The answer is reused for
228
+ * `CACHED_TOKEN_REUSE_MS` -- long enough to cover a slow or reverting tokenURI
229
+ * call on the same load (seconds), short enough that a later load re-reads
230
+ * the API. Never rejects; a failed lookup (`null`) is not reused.
231
+ *
232
+ * Costs ~17 KB more than the plain lookup on an average listing (8.8 KB ->
233
+ * 26.2 KB measured on 25 live mainnet listings), in exchange for the token's
234
+ * text ~3x sooner than the chain read (p50 0.08s vs 0.21s; seconds on
235
+ * contracts whose tokenURI RPC is slow).
236
+ */
237
+ private _fetchSharedCachedToken;
238
+ private static readonly CACHED_TOKEN_REUSE_MS;
239
+ private _cachedTokenRequest;
240
+ /** The token id the marketplace API indexes this listing's token under. */
241
+ private _tokenRef;
242
+ private static _isBurnAddress;
243
+ /**
244
+ * Apply cached media (`getTokenMedia()`) on top of a token's metadata
245
+ * (`getTokenMetadata()`). A cached URL wins only when its status is `ok`;
246
+ * otherwise the metadata's own URL is kept. Pure: returns a new Token and
247
+ * leaves both inputs untouched. `media === null` returns the token as is.
248
+ */
249
+ static applyTokenMedia(token: Token, media: TokenMedia | null): Token;
180
250
  /**
181
251
  * Fetch metadata with server fallback
182
252
  */
@@ -1218,15 +1218,36 @@ class MarketplaceListing {
1218
1218
  });
1219
1219
  }
1220
1220
  /**
1221
- * Get token details for this listing
1221
+ * Get token details for this listing: on-chain metadata with the cached,
1222
+ * CDN-optimized media applied on top.
1223
+ *
1224
+ * Composed of the independent reads below, fetched in parallel. A caller
1225
+ * that renders text and media separately should use those directly:
1226
+ * `getCachedTokenMetadata()` (fast, from the marketplace API's index) then
1227
+ * `getTokenMetadata()` (authoritative, from the chain) for text, and
1228
+ * `getTokenMedia()` for the artwork. None of them waits for another.
1222
1229
  */
1223
1230
  getTokenDetails() {
1231
+ return __awaiter(this, void 0, void 0, function* () {
1232
+ const [token, media] = yield Promise.all([this.getTokenMetadata(), this.getTokenMedia()]);
1233
+ return MarketplaceListing.applyTokenMedia(token, media);
1234
+ });
1235
+ }
1236
+ /**
1237
+ * The token's own metadata: name, description, attributes and the media URLs
1238
+ * the metadata declares, read via tokenURI (`uri` for ERC1155, `assetURI` for
1239
+ * lazy delivery). Does NOT wait for, or apply, the cached media -- see
1240
+ * `getTokenMedia()` and `applyTokenMedia()`.
1241
+ *
1242
+ * Burnt tokens (tokenURI reverts) fall back to the marketplace API's cached
1243
+ * copy, which already carries cached media URLs.
1244
+ */
1245
+ getTokenMetadata() {
1224
1246
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1225
1247
  return __awaiter(this, void 0, void 0, function* () {
1226
1248
  let tokenURI;
1227
1249
  let tokenSpec = '';
1228
1250
  let tokenId;
1229
- let tokenLazy = false;
1230
1251
  let contract;
1231
1252
  let bridgeContract;
1232
1253
  let contractFunctionName;
@@ -1253,7 +1274,6 @@ class MarketplaceListing {
1253
1274
  // Lazy token
1254
1275
  // @ts-expect-error TS2532: Object is possibly 'undefined'.
1255
1276
  tokenId = this.listing.tokenLazyAssetId;
1256
- tokenLazy = true;
1257
1277
  contract = this._getLazyDeliveryContract(false);
1258
1278
  //@ts-expect-error TS2345: Argument of type 'Contract | undefined' is not assignable to type 'Contract'.
1259
1279
  bridgeContract = this._getLazyDeliveryContract(false, true);
@@ -1265,8 +1285,9 @@ class MarketplaceListing {
1265
1285
  tokenURI = yield (0, Web3Protocol_1.callWeb3)(contract, bridgeContract, contractFunctionName, [tokenId]);
1266
1286
  }
1267
1287
  catch (error) {
1268
- // Token might be burnt - try to get cached data from API, include token details to get name, description
1269
- const cachedData = yield this._fetchCachedTokenData(tokenId, tokenLazy, true);
1288
+ // Token might be burnt - try to get cached data from API, include token details to get name, description.
1289
+ // Reuses the cached request getTokenMedia()/getCachedTokenMetadata() already have in flight.
1290
+ const cachedData = yield this._fetchSharedCachedToken();
1270
1291
  if (cachedData) {
1271
1292
  return new Token_1.default(
1272
1293
  // @ts-expect-error TS2532: Object is possibly 'undefined'.
@@ -1336,23 +1357,148 @@ class MarketplaceListing {
1336
1357
  throw error;
1337
1358
  }
1338
1359
  this._processTokenData(tokenData);
1339
- // Try to get cache optimized image
1340
- const cachedData = yield this._fetchCachedTokenData(tokenId, tokenLazy);
1341
- if (cachedData) {
1342
- if (cachedData.image_status == 'ok' && cachedData.image_url)
1343
- tokenData.image = cachedData.image_url;
1344
- if (cachedData.image_status == 'ok' && cachedData.original_image_url)
1345
- tokenData.original_image_url = cachedData.original_image_url;
1346
- if (cachedData.animation_status == 'ok' && cachedData.animation_url)
1347
- tokenData.animation_url = cachedData.animation_url;
1348
- if (cachedData.animation_status == 'ok' && cachedData.original_animation_url)
1349
- tokenData.original_animation_url = cachedData.original_animation_url;
1350
- }
1351
1360
  return new Token_1.default(
1352
1361
  // @ts-expect-error TS2532: Object is possibly 'undefined'.
1353
1362
  this.listing.tokenAddress, tokenId, tokenSpec == '', tokenData.image, tokenData.original_image_url, tokenData.animation_url, tokenData.original_animation_url, tokenData.name, tokenData.description, tokenData.attributes, tokenData, tokenURI);
1354
1363
  });
1355
1364
  }
1365
+ /**
1366
+ * The marketplace API's cached, CDN-optimized media for this listing's token
1367
+ * (`/token/<address>/<id>/media`). Needs only the token address and id, so it
1368
+ * does not wait for tokenURI or the metadata.
1369
+ *
1370
+ * Never rejects: resolves to `null` when the lookup fails or has no entry.
1371
+ * Shares one request with `getCachedTokenMetadata()` (see
1372
+ * `_fetchSharedCachedToken`).
1373
+ */
1374
+ getTokenMedia() {
1375
+ return __awaiter(this, void 0, void 0, function* () {
1376
+ const cached = yield this._fetchSharedCachedToken();
1377
+ if (!cached)
1378
+ return null;
1379
+ return {
1380
+ animation_status: cached.animation_status,
1381
+ animation_url: cached.animation_url,
1382
+ image_status: cached.image_status,
1383
+ image_url: cached.image_url,
1384
+ original_animation_url: cached.original_animation_url,
1385
+ original_image_url: cached.original_image_url,
1386
+ };
1387
+ });
1388
+ }
1389
+ /**
1390
+ * The token's name, description and attributes as last indexed by the
1391
+ * marketplace API (NFT proxy), WITHOUT reading the chain. Answers in ~0.1s;
1392
+ * `getTokenMetadata()` needs a tokenURI RPC plus the metadata fetch, and on
1393
+ * contracts that build metadata on-chain the RPC can take seconds or fail.
1394
+ *
1395
+ * For showing the token's text immediately. It is an index, not the source of
1396
+ * truth: callers must replace it with `getTokenMetadata()` when that lands.
1397
+ * Measured on 60 live mainnet listings: indexed for 39, and for all 39 the
1398
+ * name and description equalled the chain's.
1399
+ *
1400
+ * Media URLs are the metadata's own, as in `getTokenMetadata()`; cached media
1401
+ * comes from `getTokenMedia()`. Never rejects: `null` when the token is not
1402
+ * indexed or the lookup fails.
1403
+ */
1404
+ getCachedTokenMetadata() {
1405
+ var _a;
1406
+ return __awaiter(this, void 0, void 0, function* () {
1407
+ const cached = yield this._fetchSharedCachedToken();
1408
+ const details = cached === null || cached === void 0 ? void 0 : cached.tokenDetails;
1409
+ if (!details)
1410
+ return null;
1411
+ try {
1412
+ const { tokenId, lazy } = this._tokenRef();
1413
+ const tokenData = Object.assign({}, (details.metadata || {}));
1414
+ this._processTokenData(tokenData);
1415
+ return new Token_1.default(
1416
+ // @ts-expect-error TS2532: Object is possibly 'undefined'.
1417
+ this.listing.tokenAddress, tokenId, lazy, tokenData.image, tokenData.original_image_url, tokenData.animation_url, tokenData.original_animation_url, (_a = tokenData.name) !== null && _a !== void 0 ? _a : details.name, tokenData.description, tokenData.attributes, tokenData, '', MarketplaceListing._isBurnAddress(details.ownerAddress));
1418
+ }
1419
+ catch (error) {
1420
+ return null;
1421
+ }
1422
+ });
1423
+ }
1424
+ /**
1425
+ * The cached-token lookup WITH token details, shared by `getTokenMedia()`,
1426
+ * `getCachedTokenMetadata()` and the burnt-token fallback in
1427
+ * `getTokenMetadata()`: one `/media?withTokenDetails=true` request per load
1428
+ * instead of up to three. The answer is reused for
1429
+ * `CACHED_TOKEN_REUSE_MS` -- long enough to cover a slow or reverting tokenURI
1430
+ * call on the same load (seconds), short enough that a later load re-reads
1431
+ * the API. Never rejects; a failed lookup (`null`) is not reused.
1432
+ *
1433
+ * Costs ~17 KB more than the plain lookup on an average listing (8.8 KB ->
1434
+ * 26.2 KB measured on 25 live mainnet listings), in exchange for the token's
1435
+ * text ~3x sooner than the chain read (p50 0.08s vs 0.21s; seconds on
1436
+ * contracts whose tokenURI RPC is slow).
1437
+ */
1438
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1439
+ _fetchSharedCachedToken() {
1440
+ const now = Date.now();
1441
+ const shared = this._cachedTokenRequest;
1442
+ if (shared && now - shared.startedAt < MarketplaceListing.CACHED_TOKEN_REUSE_MS) {
1443
+ return shared.request;
1444
+ }
1445
+ const { tokenId, lazy } = this._tokenRef();
1446
+ const request = this._fetchCachedTokenData(tokenId, lazy, true);
1447
+ const entry = { request, startedAt: now };
1448
+ this._cachedTokenRequest = entry;
1449
+ request.then((result) => {
1450
+ if (result == null && this._cachedTokenRequest === entry)
1451
+ this._cachedTokenRequest = undefined;
1452
+ });
1453
+ return request;
1454
+ }
1455
+ /** The token id the marketplace API indexes this listing's token under. */
1456
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1457
+ _tokenRef() {
1458
+ if (this.listing instanceof Listing_1.TokenListing)
1459
+ return { lazy: false, tokenId: this.listing.tokenId };
1460
+ // @ts-expect-error TS2532: Object is possibly 'undefined'.
1461
+ return { lazy: true, tokenId: this.listing.tokenLazyAssetId };
1462
+ }
1463
+ static _isBurnAddress(address) {
1464
+ const a = address === null || address === void 0 ? void 0 : address.toLowerCase();
1465
+ return (a === '0x000000000000000000000000000000000000dead' ||
1466
+ a === '0x0000000000000000000000000000000000000000');
1467
+ }
1468
+ /**
1469
+ * Apply cached media (`getTokenMedia()`) on top of a token's metadata
1470
+ * (`getTokenMetadata()`). A cached URL wins only when its status is `ok`;
1471
+ * otherwise the metadata's own URL is kept. Pure: returns a new Token and
1472
+ * leaves both inputs untouched. `media === null` returns the token as is.
1473
+ */
1474
+ static applyTokenMedia(token, media) {
1475
+ if (!media)
1476
+ return token;
1477
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1478
+ const tokenData = token.tokenData ? Object.assign({}, token.tokenData) : token.tokenData;
1479
+ let { image_url, original_image_url, animation_url, original_animation_url } = token;
1480
+ if (media.image_status == 'ok' && media.image_url) {
1481
+ image_url = media.image_url;
1482
+ if (tokenData)
1483
+ tokenData.image = media.image_url;
1484
+ }
1485
+ if (media.image_status == 'ok' && media.original_image_url) {
1486
+ original_image_url = media.original_image_url;
1487
+ if (tokenData)
1488
+ tokenData.original_image_url = media.original_image_url;
1489
+ }
1490
+ if (media.animation_status == 'ok' && media.animation_url) {
1491
+ animation_url = media.animation_url;
1492
+ if (tokenData)
1493
+ tokenData.animation_url = media.animation_url;
1494
+ }
1495
+ if (media.animation_status == 'ok' && media.original_animation_url) {
1496
+ original_animation_url = media.original_animation_url;
1497
+ if (tokenData)
1498
+ tokenData.original_animation_url = media.original_animation_url;
1499
+ }
1500
+ return new Token_1.default(token.address, token.id, token.lazy, image_url, original_image_url, animation_url, original_animation_url, token.name, token.description, token.attributes, tokenData, token.originalURI, token.burnt);
1501
+ }
1356
1502
  /**
1357
1503
  * Fetch metadata with server fallback
1358
1504
  */
@@ -1409,11 +1555,15 @@ class MarketplaceListing {
1409
1555
  try {
1410
1556
  const apiResponse = yield fetch(`${this._getEndpoint()}/token/${(_a = this.listing) === null || _a === void 0 ? void 0 : _a.tokenAddress}/${tokenId}/media?${((_b = this.overrides) === null || _b === void 0 ? void 0 : _b.mediaOptimization) ? this.overrides.mediaOptimization : 'max_width=1280'}${tokenLazy ? '&lazy=true' : ''}${withTokenDetails ? '&withTokenDetails=true' : ''}`);
1411
1557
  if (apiResponse.status === 200) {
1412
- return apiResponse.json();
1558
+ // `return await`, not `return`: a bare return hands back the json()
1559
+ // promise un-awaited, so a malformed 200 body rejected PAST this catch.
1560
+ // getTokenDetails() starts this lookup in parallel and does not await it
1561
+ // on every path, so it must never reject.
1562
+ return yield apiResponse.json();
1413
1563
  }
1414
1564
  }
1415
1565
  catch (error) {
1416
- // Return null if fetch fails
1566
+ // Return null if fetch or body parsing fails
1417
1567
  }
1418
1568
  return null;
1419
1569
  });
@@ -1442,6 +1592,14 @@ class MarketplaceListing {
1442
1592
  _processTokenData(data) {
1443
1593
  if (data === null || data === void 0 ? void 0 : data.image) {
1444
1594
  data.image = this._cleanURI(data.image);
1595
+ // Handle video files incorrectly placed in the image field
1596
+ if (js_ts_utils_1.Media.isVideo.test(data.image) && !data.animation_url) {
1597
+ // Move video from image to animation_url
1598
+ data.animation_url = data.image;
1599
+ data.original_animation_url = data.image;
1600
+ // Clear image field or use a fallback
1601
+ data.image = undefined;
1602
+ }
1445
1603
  }
1446
1604
  if (data === null || data === void 0 ? void 0 : data.animation_url) {
1447
1605
  data.animation_url = this._cleanURI(data.animation_url);
@@ -1547,3 +1705,4 @@ MarketplaceListing._fetchWithTimeout = (url, options, timeout = 7000) => {
1547
1705
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
1548
1706
  ]);
1549
1707
  };
1708
+ MarketplaceListing.CACHED_TOKEN_REUSE_MS = 30000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manifoldxyz/marketplace-sdk",
3
- "version": "4.0.2",
3
+ "version": "4.1.0",
4
4
  "typings": "dist/index.d.ts",
5
5
  "description": "",
6
6
  "main": "dist/index.js",
@@ -48,7 +48,7 @@
48
48
  "@manifoldxyz/js-ts-utils": "^7.5.0",
49
49
  "@ethersproject/units": "^5.7.0",
50
50
  "@manifoldxyz/contract-abis": "^1.0.0",
51
- "@manifoldxyz/ens-batch-lookup-ethers": "^0.0.2",
51
+ "@manifoldxyz/ens-batch-lookup-ethers": "^0.1.1",
52
52
  "@manifoldxyz/frontend-provider-types": "^2.1.0",
53
53
  "@manifoldxyz/manifold-provider-client": "^0.2.0",
54
54
  "keccak256": "^1.0.6",