@manifoldxyz/dropkit 0.2.1 → 0.4.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
@@ -97,6 +97,48 @@ function erc20ContractConfig(chainId: number, address: Address, paymentToken: Ad
97
97
  The example charges 1,000,000 token base units per mint. The deployed contract must spend that token from the payer.
98
98
  The adapter reads allowance and creates an approval leg when required. Manifold claims discover their payment token on-chain instead.
99
99
 
100
+ ## Transient Labs drops
101
+
102
+ `tl-stacks` sells a drop that lives on Transient Labs (TL) Stacks 721 (v2.3.0 or v2.3.1) from your own page. Write three values; everything else is read from the chain.
103
+
104
+ ```tsx
105
+ import { MintButton, type MintResult, type TlStacksConfig } from '@manifoldxyz/dropkit';
106
+
107
+ const boneMarket: TlStacksConfig = {
108
+ type: 'tl-stacks',
109
+ chainId: 1,
110
+ stacks: '0x384092784cfaa91efaa77870c04d958e20840242',
111
+ nftContract: '0x8da32357980b32d13f91c607ac52712d96f5e656',
112
+ // Only a presale needs this. See "Where allowlistUrl comes from".
113
+ allowlistUrl: 'https://example.com/stacks/drop-id/allowlist',
114
+ };
115
+
116
+ export function Mint({ rpcUrl }: { rpcUrl: string }) {
117
+ return <MintButton config={boneMarket} rpcUrls={{ 1: rpcUrl }} />;
118
+ }
119
+
120
+ /** "Minted 1 of 5. The rest was refunded." when the sale minted fewer than asked. */
121
+ export function mintedLine(result: MintResult): string {
122
+ const minted = result.tokens.length;
123
+ return minted < result.requested ? `Minted ${minted} of ${result.requested}. The rest was refunded.` : `Minted ${minted}.`;
124
+ }
125
+ ```
126
+
127
+ - `stacks` is TL's sale contract. It is `0x384092784cfaa91efaa77870c04d958e20840242` on Ethereum, Base, Arbitrum, Shape, Sepolia and Base Sepolia. It has no default: a TL redeploy must be a config change.
128
+ - `nftContract` is the creator's TL contract (ERC721TL, ERC7160TL or ERC7160TLEditions). TL keeps one drop per NFT contract.
129
+ - The button shows the price for the current phase, TL's fee, supply left, the phase times and the per-wallet limit, all read from the chain. It refreshes by itself when the presale ends, so the price changes from presale to public with no reload.
130
+ - `rpcUrls`: the Manifold read bridge only serves Manifold contracts. Pass your own domain-restricted RPC key for the drop's chain. Without it, reads use the chain's public RPC.
131
+
132
+ **Where `allowlistUrl` comes from.** TL publishes an allowlist file for every presale. Its URL is the `allowlistUrl` field in the drop's page data on transient.xyz. It is an OpenZeppelin merkle tree dump (`format: "standard-v1"`, leaves `[keccak256(abi.encode(wallet)), allowance]`). A site can also host a copy in the same format. The button loads it once per page, only in the presale and only after a wallet connects, checks that its root equals the drop's on-chain `presaleMerkleRoot`, and builds the wallet's proof in the browser. Nothing is asked of the collector or the creator. A presale with no `allowlistUrl`, an unreachable file or a root that does not match fails with `ALLOWLIST_UNAVAILABLE`; the button never sends a presale mint it cannot prove. The public sale does not use the file. A page left open while TL changes the allowlist needs a reload.
133
+
134
+ **Limits (v1).** ERC-721 drops priced in the chain's native token only. No ERC-1155, ERC-20 prices, ERC-7160 version picker, Relay cross-chain payment or cart: a `tl-stacks` config in the cart is rejected with `INVALID_CONFIG`. Other Stacks versions are `unsupported` with `INVALID_CONFIG`. A chain with no drop for the NFT contract is `unsupported` with `DROP_NOT_FOUND`.
135
+
136
+ **Fee.** For `tl-stacks`, `DropInfo.unitFee` is TL's protocol fee, not a Manifold fee. Label it "TL fee" in a custom UI. The collector pays `quantity × (unitPrice + unitFee)`.
137
+
138
+ **Partial mints.** TL mints fewer tokens than asked when the wallet limit or the supply left is lower, and refunds the rest in the same transaction. `MintResult.requested` is the quantity asked for and `tokens.length` is what was minted, so a site can show "Minted 1 of 5. The rest was refunded." as above. For Manifold claims and contract drops `requested` equals the quantity, so their results read as before. A refund to a wallet that rejects a 100k-gas ETH transfer, such as some smart wallets, arrives as WETH instead of ETH.
139
+
140
+ **Price changes.** The amount sent is the last drop read before the click. If the price or fee went up since, the simulation fails and the next refresh shows the new amount. If it went down, TL refunds the difference.
141
+
100
142
  ## React API
101
143
 
102
144
  `useMint(options)` and `MintButton` accept:
@@ -148,7 +190,7 @@ export function CustomMint({ config }: { config: MintConfig }) {
148
190
  | `total` | `{ native: Money, erc20: Money \| null }`, or null before the drop loads. Includes the Manifold fee. |
149
191
  | `mint` | Stable callback. Connects, submits an attempt, or resets a terminal state, depending on `state`. |
150
192
  | `refresh` | Stable callback returning `Promise<void>`. Re-reads drop data. Queues refreshes during an attempt. |
151
- | `result` | `{ txHash, explorerUrl, tokens }` after success, otherwise null. Token IDs and amounts are bigint. |
193
+ | `result` | `{ txHash, explorerUrl, tokens, requested }` after success, otherwise null. Token IDs and amounts are bigint. `requested` is the quantity asked for. |
152
194
  | `canAct` | Whether a click can act. False during an attempt, including its pre-prompt planning phase. |
153
195
  | `route` | `{ kind: 'direct', chainId }` or `{ kind: 'relay', originChainId, amountIn, relayFee, originGas, quotedAt }`. Null before the wallet read. |
154
196
  | `crossChain` | Inputs for `PayWithPicker`. Null when Relay cannot mint this drop for this wallet. |
@@ -197,6 +239,7 @@ ERC20 totals include the token price and any native fee. Gas is checked separate
197
239
  | `UNKNOWN_EXTENSION` | `unsupported` | Extension is not supported on this chain. |
198
240
  | `AMBIGUOUS_EXTENSION` | `unsupported` | Discovery found multiple claims. Supply `extension`. |
199
241
  | `CLAIM_NOT_FOUND` | `unsupported` | No supported extension contains this claim. |
242
+ | `DROP_NOT_FOUND` | `unsupported` | `tl-stacks` only: Stacks has no drop configured for this NFT contract. |
200
243
  | `SIGNATURE_CLAIM` | `unsupported` | Signature or redemption-code minting is not supported. |
201
244
  | `INVALID_CONFIG` | `unsupported` | Invalid field, ABI, function, placeholder, or chain. Read `message`. |
202
245
  | `CONNECT_WIDGET_TOO_OLD` | `unsupported` | Host widget lacks the required modal API. |
@@ -274,6 +317,58 @@ export function CrossChainMint({ config }: { config: MintConfig }) {
274
317
 
275
318
  Rows expose `data-chain`, `data-selected`, `data-loading` and `aria-pressed`. `renderRow` replaces the row content.
276
319
 
320
+ ## Cart checkout
321
+
322
+ `@manifoldxyz/dropkit/cart` collects several Manifold claims in one cart and sends one mint transaction per item. Importing only `@manifoldxyz/dropkit` never loads the cart. The cart takes Manifold claims only, all on one chain.
323
+
324
+ ```tsx
325
+ import { AddToCartButton, CartProvider, useCart, useCheckout } from '@manifoldxyz/dropkit/cart';
326
+ import type { ManifoldClaimConfig } from '@manifoldxyz/dropkit';
327
+
328
+ const claim: ManifoldClaimConfig = {
329
+ type: 'manifold-claim',
330
+ chainId: 1,
331
+ creatorContract: '0x0000000000000000000000000000000000000001',
332
+ instanceId: '1',
333
+ extension: '0x0000000000000000000000000000000000000002', // required in the cart
334
+ };
335
+
336
+ function Checkout() {
337
+ const cart = useCart();
338
+ const checkout = useCheckout();
339
+ return (
340
+ <div>
341
+ <p>{cart.count} in cart</p>
342
+ {checkout.state === 'idle' && <button onClick={checkout.checkout}>Check out</button>}
343
+ {checkout.state === 'ready' && <button onClick={checkout.confirm}>Confirm in wallet</button>}
344
+ {checkout.state === 'checking' && <button onClick={checkout.check}>Check again</button>}
345
+ {checkout.message && <p>{checkout.message}</p>}
346
+ </div>
347
+ );
348
+ }
349
+
350
+ export function Shop() {
351
+ return (
352
+ <CartProvider chainId={1} rpcUrls={{ 1: 'https://eth.example/rpc' }}>
353
+ <AddToCartButton config={claim} />
354
+ <Checkout />
355
+ </CartProvider>
356
+ );
357
+ }
358
+ ```
359
+
360
+ Things to know:
361
+
362
+ - **`extension` is required** on every cart config. Without it `add()` throws `INVALID_CONFIG`.
363
+ - **Pass `rpcUrls`.** Without an RPC URL for the cart's chain, the provider logs a console warning once: receipts, holdings and balance checks then use a public RPC that can be rate-limited during a sale.
364
+ - **One `CartProvider` per `storageKey` per tab.** The default key is `dropkit-cart:<chainId>`. A second provider with the same key throws `INVALID_CONFIG`.
365
+ - **What the cart keeps in `sessionStorage`:** one JSON value under the storage key holding the items (config and quantity), the mints that were sent and are not yet settled (payer, recipient, quantity, transaction hash and the holdings count taken before sending), and the results shown to the collector. It holds no keys and no signatures. It lives for the tab session. If `sessionStorage` is unavailable or full, checkout stops with `PERSISTENCE_UNAVAILABLE` before anything is sent.
366
+ - **Each click makes one wallet request:** switch network, add network, or one mint. `confirm()` must run from a click handler.
367
+ - **`useHoldings(configs, { wallet? })`** reads what a wallet holds of each drop, for "You own N" on a drop page: `get(config)` returns `{ kind, count }`: `owned` for an ERC-1155 drop (the token balance, transfers included), `minted` for an ERC-721 drop with a wallet limit (mints from that drop), and `owned-in-contract` for other ERC-721 drops (the balance on the whole contract, which equals `owned` when the contract holds only this drop). It reads the connected wallet by default, on mount, on a wallet change, after each collected mint and on `refresh()`, with no timer. Use it inside `CartProvider`.
368
+ - **Links for collected items.** Each `cart.collected` entry has `tokens` (from the receipt) and `links`: `tx` (block explorer), `tokens` (one OpenSea item page each) and `profile`. Pass `openseaCollections={{ '0xYourContract': 'your-opensea-slug' }}` to `CartProvider` and `profile` opens the collector's OpenSea holdings filtered to your collection. Without a slug, `profile` is the plain OpenSea profile when no token id is known (a lost receipt on a 721), else null. OpenSea has no testnets, so Sepolia gets the explorer link only.
369
+ - **A mint that cannot be confirmed** is never retried for you. It is listed in `cart.unconfirmed` until the collector dismisses it or adds the item back. Each entry has `wallet` (the wallet that sent it) and `message`, which names that wallet: "We couldn't confirm this. Check wallet 0x1234…abcd. If it didn't go through, add it again."
370
+ - **Any wallet works**, smart accounts included. A wallet that returns a request id instead of a transaction hash (a Safe through WalletConnect, for example) shows "Still confirming", then settles from the collector's holdings: Collected if the mint landed, otherwise Unconfirmed. The cart never resends it.
371
+
277
372
  ## Local verification
278
373
 
279
374
  From the repository root, use Node 24 and pnpm 10.34.5:
@@ -282,13 +377,13 @@ From the repository root, use Node 24 and pnpm 10.34.5:
282
377
  pnpm install --frozen-lockfile
283
378
  cd packages/react/dropkit
284
379
  pnpm test # fast unit and component tests, no network
285
- pnpm test:chain # adapters against a local Sepolia fork
380
+ pnpm test:chain # adapters against local Sepolia and Ethereum mainnet forks
286
381
  pnpm run typecheck
287
382
  pnpm run lint
288
383
  pnpm run build
289
384
  pnpm verify:package # packed tarball in clean React 18 and 19 consumers
290
385
  ```
291
386
 
292
- `test:chain` requires `anvil` on `PATH` and `SEPOLIA_RPC_URL`. It forks block 11838500 and fails rather than skips when either is missing.
387
+ `test:chain` requires `anvil` on `PATH`, `SEPOLIA_RPC_URL` and `MAINNET_RPC_URL`. The Sepolia tests fork block 11838500. The Transient Labs test forks Ethereum mainnet at blocks 26234702 and 26235901, so `MAINNET_RPC_URL` must be an archive endpoint. It fails rather than skips when any is missing.
293
388
  `verify:package` needs npm registry access. It type-checks every TypeScript example in this README.
294
389
  The repository's pnpm policy enforces a seven-day release age, except for `@manifoldxyz/*` packages.