@manifoldxyz/marketplace-sdk 4.0.3 → 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 +168 -4
- package/dist/classes/Token.d.ts +14 -0
- package/dist/index.d.ts +2 -2
- package/dist/lib/MarketplaceListing.d.ts +72 -2
- package/dist/lib/MarketplaceListing.js +170 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,171 @@
|
|
|
1
|
-
# marketplace-sdk
|
|
1
|
+
# @manifoldxyz/marketplace-sdk
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## Prerequisites
|
|
6
6
|
|
|
7
|
-
|
|
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
|
+
```
|
package/dist/classes/Token.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
});
|
|
@@ -1555,3 +1705,4 @@ MarketplaceListing._fetchWithTimeout = (url, options, timeout = 7000) => {
|
|
|
1555
1705
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
|
|
1556
1706
|
]);
|
|
1557
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
|
|
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.
|
|
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",
|