@manifoldxyz/dropkit 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Manifold
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,246 @@
1
+ # @manifoldxyz/dropkit
2
+
3
+ `MintButton` renders an unstyled mint button. `useMint` provides the same transaction flow for custom interfaces.
4
+ Both support Manifold claims and ABI-configured contracts. Runtime peers are React 18+, React DOM 18+, and viem 2.
5
+
6
+ ## Agent recipe
7
+
8
+ 1. Obtain the chain ID, creator contract, and Studio instance ID from the creator's drop. Do not infer these from artwork URLs.
9
+ 2. Use `type: 'manifold-claim'` for a supported Manifold claim. Add `extension` if discovery finds multiple matches.
10
+ 3. For another contract, use `type: 'contract'` with its actual ABI and mint argument template. Verify the payable function and price units.
11
+ 4. Pass a domain-restricted RPC URL through `rpcUrls`. Never include a private key or an unrestricted secret in browser code.
12
+ 5. Render `MintButton`, or use its function child for custom markup. Call `mint` from a user gesture, never from an effect.
13
+ 6. Test disconnected, blocked, rejected, and successful paths before a release. Confirm the recipient's tokens on-chain.
14
+
15
+ ```tsx
16
+ import { useState } from 'react';
17
+ import { MintButton, type ManifoldClaimConfig } from '@manifoldxyz/dropkit';
18
+
19
+ // Supply verified deployment values from the host application.
20
+ export function Claim({ config, rpcUrl }: { config: ManifoldClaimConfig; rpcUrl: string }) {
21
+ const [quantity, setQuantity] = useState(1);
22
+ return (
23
+ <MintButton
24
+ config={config}
25
+ rpcUrls={{ [config.chainId]: rpcUrl }}
26
+ quantity={quantity}
27
+ onQuantityChange={setQuantity}
28
+ className="mint"
29
+ labels={{ live: 'Collect', 'sold-out': 'Gone' }}
30
+ onEvent={(event) => console.info(event.type, event.state)}
31
+ />
32
+ );
33
+ }
34
+ ```
35
+
36
+ A Manifold config has this shape:
37
+
38
+ ```ts
39
+ import type { Address, ManifoldClaimConfig } from '@manifoldxyz/dropkit';
40
+
41
+ function claimConfig(chainId: number, creatorContract: Address, instanceId: string): ManifoldClaimConfig {
42
+ return { type: 'manifold-claim', chainId, creatorContract, instanceId };
43
+ }
44
+ ```
45
+
46
+ `instanceId` is a decimal string. Eligibility belongs to `recipient`, which defaults to the connected wallet.
47
+ The connected wallet pays for the mint and gas. Public claims read on-chain data without calling the Manifold allowlist API.
48
+
49
+ ## Contract config
50
+
51
+ ```ts
52
+ import { parseAbi } from 'viem';
53
+ import type { Address, ContractConfig } from '@manifoldxyz/dropkit';
54
+
55
+ function contractConfig(chainId: number, address: Address): ContractConfig {
56
+ return {
57
+ type: 'contract',
58
+ chainId,
59
+ address,
60
+ abi: parseAbi([
61
+ 'function mint(address to, uint256 quantity) payable',
62
+ 'function price() view returns (uint256)',
63
+ 'function totalSupply() view returns (uint256)',
64
+ 'function maxSupply() view returns (uint256)',
65
+ ]),
66
+ mint: { functionName: 'mint', args: ['{recipient}', '{quantity}'] },
67
+ price: { read: { functionName: 'price' }, currency: 'native' },
68
+ totalMinted: { read: { functionName: 'totalSupply' } },
69
+ maxSupply: { read: { functionName: 'maxSupply' } },
70
+ };
71
+ }
72
+ ```
73
+
74
+ This example requires a contract with those exact functions. `tokenContract` overrides the address whose mint logs are parsed.
75
+ Price currency is `'native'` or `{ address: erc20Address }`. ERC20 symbols and decimals come from the token contract.
76
+ `price.per` defaults to `'unit'`; `'total'` means the price applies to the entire transaction.
77
+
78
+ Numeric fields accept `{ value: bigint }` in base units, or `{ read: { functionName, args?, output? } }`.
79
+ Price also accepts `{ value: '0.01', unit: 'display', currency: 'native' }`.
80
+ Read `output` selects a tuple member by index or name. Times are Unix seconds. Absent or zero maximum supply means an open edition.
81
+ Optional fields include `walletMinted`, `walletMax`, `startTime`, and `endTime`.
82
+
83
+ Argument templates support `{recipient}`, `{payer}`, `{quantity}`, `{unitPrice}`, and `{totalPrice}`.
84
+ Use bigint or decimal strings for integers beyond `Number.MAX_SAFE_INTEGER`.
85
+
86
+ For an ERC20-priced contract with the same mint function, replace the native price with a token price:
87
+
88
+ ```ts
89
+ function erc20ContractConfig(chainId: number, address: Address, paymentToken: Address): ContractConfig {
90
+ return {
91
+ ...contractConfig(chainId, address),
92
+ price: { value: 1_000_000n, currency: { address: paymentToken } },
93
+ };
94
+ }
95
+ ```
96
+
97
+ The example charges 1,000,000 token base units per mint. The deployed contract must spend that token from the payer.
98
+ The adapter reads allowance and creates an approval leg when required. Manifold claims discover their payment token on-chain instead.
99
+
100
+ ## React API
101
+
102
+ `useMint(options)` and `MintButton` accept:
103
+
104
+ | Option | Behavior |
105
+ | --- | --- |
106
+ | `config` | Required `MintConfig` discriminated union. |
107
+ | `rpcUrls` | Optional map of chain IDs to JSON-RPC URLs. |
108
+ | `recipient` | Optional receiving address. Defaults to payer. |
109
+ | `quantity` | Initial quantity, or controlled quantity with `onQuantityChange`. Defaults to 1. |
110
+ | `onQuantityChange` | Receives the clamped requested quantity. The parent must update `quantity`. |
111
+ | `connect` | Optional `{ walletConnectProjectId }` forwarded to the shared connect widget. |
112
+ | `onEvent` | Optional event callback. No telemetry is sent by the package. |
113
+
114
+ `MintButton` additionally accepts `className`, `labels`, and `children`.
115
+ A function child receives the full `MintState` and replaces the default button.
116
+
117
+ ```tsx
118
+ import { MintButton, type MintConfig } from '@manifoldxyz/dropkit';
119
+
120
+ export function CustomMint({ config }: { config: MintConfig }) {
121
+ return (
122
+ <MintButton config={config}>
123
+ {(m) => (
124
+ <section>
125
+ <button type="button" onClick={m.mint} disabled={!m.canAct} data-state={m.state}>
126
+ {m.state === 'success' ? 'Minted' : m.state === 'disconnected' ? 'Connect wallet' : 'Mint'}
127
+ </button>
128
+ <p role="status">{m.message}</p>
129
+ {m.result?.explorerUrl && <a href={m.result.explorerUrl}>View transaction</a>}
130
+ </section>
131
+ )}
132
+ </MintButton>
133
+ );
134
+ }
135
+ ```
136
+
137
+ `useMint` returns:
138
+
139
+ | Field | Type and meaning |
140
+ | --- | --- |
141
+ | `state`, `reason`, `message` | State name, nullable reason code, and nullable human-readable detail. |
142
+ | `drop` | `DropInfo` or null before reads finish. Includes price, fee, supply, times, payment token, and allowlist flag. |
143
+ | `wallet` | `WalletInfo` or null until wallet reads finish. Includes payer, recipient, chain, and mint limits. |
144
+ | `quantity`, `setQuantity` | Current quantity and a setter that clamps requests to the supported range. |
145
+ | `maxQuantity` | Minimum of remaining supply, wallet or allowlist limit, and 65535. Null before the drop loads. |
146
+ | `total` | `{ native: Money, erc20: Money \| null }`, or null before the drop loads. Includes the Manifold fee. |
147
+ | `mint` | Stable callback. Connects, submits an attempt, or resets a terminal state, depending on `state`. |
148
+ | `refresh` | Stable callback returning `Promise<void>`. Re-reads drop data. Queues refreshes during an attempt. |
149
+ | `result` | `{ txHash, explorerUrl, tokens }` after success, otherwise null. Token IDs and amounts are bigint. |
150
+ | `canAct` | Whether a click can act. False during an attempt, including its pre-prompt planning phase. |
151
+
152
+ `Money` is `{ value: bigint, decimals: number, symbol: string, formatted: string }`.
153
+ The component ships no CSS. The default button exposes `data-state`, `data-reason`, `disabled`, and `aria-busy` during asynchronous states.
154
+
155
+ ```css
156
+ .mint { padding: 0.75rem 1.25rem; border: 1px solid currentColor; border-radius: 0.5rem; }
157
+ .mint:disabled { cursor: not-allowed; opacity: 0.6; }
158
+ .mint[aria-busy="true"] { cursor: progress; }
159
+ .mint[data-state="success"] { color: #176b39; }
160
+ .mint[data-reason="INSUFFICIENT_FUNDS"] { color: #983b18; }
161
+ ```
162
+
163
+ ## States and labels
164
+
165
+ Only `disconnected`, idle `live`, `success`, and `error` accept actions.
166
+ A reset click never sends another transaction. Fresh drop and wallet reads must finish before a later mint click.
167
+
168
+ | State | Default label | Action |
169
+ | --- | --- | --- |
170
+ | `loading` | Loading… | None |
171
+ | `unsupported` | Unavailable | None |
172
+ | `upcoming` | Starts {local date} | None |
173
+ | `ended` | Ended | None |
174
+ | `sold-out` | Sold out | None |
175
+ | `disconnected` | Connect wallet | Open connect modal |
176
+ | `connecting` | Connecting… | None |
177
+ | `live` | Mint · {total} | Switch chain if needed, then approve and mint |
178
+ | `ineligible` | Not eligible | None |
179
+ | `insufficient-funds` | Insufficient {symbol} | None |
180
+ | `switching-chain` | Switching network… | None |
181
+ | `approving` | Approving {symbol}… | None |
182
+ | `minting` | Confirm in wallet… | None |
183
+ | `confirming` | Minting… | None |
184
+ | `success` | Minted | Reset and re-read |
185
+ | `error` | Try again | Reset and re-read |
186
+
187
+ ERC20 totals include the token price and any native fee. Gas is checked separately and is not included in the label.
188
+
189
+ ## Reason codes
190
+
191
+ | Reason | State | Meaning |
192
+ | --- | --- | --- |
193
+ | `UNKNOWN_EXTENSION` | `unsupported` | Extension is not supported on this chain. |
194
+ | `AMBIGUOUS_EXTENSION` | `unsupported` | Discovery found multiple claims. Supply `extension`. |
195
+ | `CLAIM_NOT_FOUND` | `unsupported` | No supported extension contains this claim. |
196
+ | `SIGNATURE_CLAIM` | `unsupported` | Signature or redemption-code minting is not supported. |
197
+ | `INVALID_CONFIG` | `unsupported` | Invalid field, ABI, function, placeholder, or chain. Read `message`. |
198
+ | `CONNECT_WIDGET_TOO_OLD` | `unsupported` | Host widget lacks the required modal API. |
199
+ | `NOT_ON_ALLOWLIST` | `ineligible` | Recipient has no allowlist allocation. |
200
+ | `ALLOWLIST_USED` | `ineligible` | Recipient used all allocated indices. |
201
+ | `WALLET_LIMIT` | `ineligible` | Recipient reached the wallet limit. |
202
+ | `SIMULATION_REVERTED` | `ineligible` | Preflight call failed. `message` contains the available revert detail. |
203
+ | `INSUFFICIENT_FUNDS` | `insufficient-funds` | Native payment plus gas, or ERC20 balance, is insufficient. |
204
+ | `USER_REJECTED` | `error` | Wallet rejected or cancelled the request, or the selected payer changed before a prompt. |
205
+ | `TX_REVERTED` | `error` | Mined transaction failed. `message` includes its hash. |
206
+ | `RATE_LIMITED` | `error` | Upstream rate limit. Retry after the provider's cooldown. |
207
+ | `RPC_ERROR` | `error` | Read or transaction transport failed. Supply a working RPC URL. |
208
+ | `CHAIN_SWITCH_FAILED` | `error` | Wallet could not switch to, or did not remain on, the configured chain. |
209
+ | `ALLOWLIST_UNAVAILABLE` | `error` | Required allowlist data could not be loaded. |
210
+
211
+ ## Events and lifecycle
212
+
213
+ `onEvent` receives `{ type, state, reason, chainId, quantity, config: { type, chainId, address }, txHash? }`.
214
+ Types are `state`, `attempt`, `approve-submitted`, `tx-submitted`, `success`, and `error`.
215
+ The config address is the mint target for contract configs and the creator contract for Manifold claims.
216
+ Transaction events use the frozen attempt, not later props. Payloads omit payer and recipient addresses.
217
+
218
+ Every hook owns its reducer, read generations, and attempt guard. Buttons share only the connect widget.
219
+ Server rendering produces a loading button without wallet calls or adapter reads.
220
+ Drop data refreshes every 30 seconds while visible, on visibility changes, and at start or end boundaries.
221
+
222
+ An attempt freezes config, payer, recipient, quantity, chain, and token contract.
223
+ Prop changes and background refreshes wait until the attempt finishes.
224
+ Unmount aborts the adapter lifetime and prevents subsequent prompts or callbacks. A submitted transaction cannot be cancelled by unmounting.
225
+
226
+ Approval is simulated before its prompt. A successful approval receipt is followed by fresh eligibility checks and mint simulation.
227
+ Receipt parsing filters mint transfers by token contract and recipient. Cancellation or changed transaction intent reports an error; a speed-up uses the replacement hash.
228
+
229
+ No direct analytics requests or `/public/instance/tx` posts are made.
230
+ Manifold allowlist calls are GET requests. JSON-RPC reads and wallet transactions still use their normal transports.
231
+
232
+ ## Local verification
233
+
234
+ From the repository root, use Node 24 and put Foundry binaries in `PATH`:
235
+
236
+ ```sh
237
+ pnpm install --frozen-lockfile
238
+ cd packages/react/dropkit
239
+ pnpm exec vitest run
240
+ pnpm run typecheck
241
+ pnpm run lint
242
+ pnpm run build
243
+ ```
244
+
245
+ The full tests require `SEPOLIA_RPC_URL` and `anvil`. Fork tests use block 11838500 and fail rather than skip when prerequisites are missing.
246
+ The repository's pnpm policy enforces a seven-day release age, except for `@manifoldxyz/*` packages.