@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 +21 -0
- package/README.md +246 -0
- package/dist/index.cjs +2396 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +207 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +2384 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2384 @@
|
|
|
1
|
+
// src/react/useMint.ts
|
|
2
|
+
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
3
|
+
import { BaseError as BaseError3, ExecutionRevertedError, publicActions } from "viem";
|
|
4
|
+
|
|
5
|
+
// src/core/rpc/index.ts
|
|
6
|
+
import { createPublicClient, http } from "viem";
|
|
7
|
+
import { apeChain, base, mainnet, optimism, sepolia, shape } from "viem/chains";
|
|
8
|
+
|
|
9
|
+
// src/core/errors.ts
|
|
10
|
+
var UNSUPPORTED_REASONS = [
|
|
11
|
+
"UNKNOWN_EXTENSION",
|
|
12
|
+
"AMBIGUOUS_EXTENSION",
|
|
13
|
+
"CLAIM_NOT_FOUND",
|
|
14
|
+
"SIGNATURE_CLAIM",
|
|
15
|
+
"INVALID_CONFIG",
|
|
16
|
+
"CONNECT_WIDGET_TOO_OLD"
|
|
17
|
+
];
|
|
18
|
+
var INELIGIBLE_REASONS = ["NOT_ON_ALLOWLIST", "ALLOWLIST_USED", "WALLET_LIMIT", "SIMULATION_REVERTED"];
|
|
19
|
+
var ERROR_REASONS = [
|
|
20
|
+
"USER_REJECTED",
|
|
21
|
+
"TX_REVERTED",
|
|
22
|
+
"RATE_LIMITED",
|
|
23
|
+
"RPC_ERROR",
|
|
24
|
+
"CHAIN_SWITCH_FAILED",
|
|
25
|
+
"ALLOWLIST_UNAVAILABLE"
|
|
26
|
+
];
|
|
27
|
+
var FUNDS_REASONS = ["INSUFFICIENT_FUNDS"];
|
|
28
|
+
var REASON_CODES = [
|
|
29
|
+
...UNSUPPORTED_REASONS,
|
|
30
|
+
...INELIGIBLE_REASONS,
|
|
31
|
+
...ERROR_REASONS,
|
|
32
|
+
...FUNDS_REASONS
|
|
33
|
+
];
|
|
34
|
+
var MintError = class extends Error {
|
|
35
|
+
reason;
|
|
36
|
+
/** INVALID_CONFIG only: dotted path of the bad field, e.g. "mint.args[1]" or "price". */
|
|
37
|
+
field;
|
|
38
|
+
constructor(reason, message, opts) {
|
|
39
|
+
super(message, opts?.cause === void 0 ? void 0 : { cause: opts.cause });
|
|
40
|
+
this.name = "MintError";
|
|
41
|
+
this.reason = reason;
|
|
42
|
+
if (opts?.field !== void 0) this.field = opts.field;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function isMintError(e) {
|
|
46
|
+
return e instanceof MintError;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/core/rpc/index.ts
|
|
50
|
+
var BRIDGE_URLS = {
|
|
51
|
+
1: "https://bridge.manifoldxyz.dev/rpc",
|
|
52
|
+
11155111: "https://sepolia.bridge.manifoldxyz.dev/rpc",
|
|
53
|
+
8453: "https://base.bridge.manifoldxyz.dev/rpc",
|
|
54
|
+
10: "https://optimism.bridge.manifoldxyz.dev/rpc",
|
|
55
|
+
360: "https://shape.bridge.manifoldxyz.dev/rpc",
|
|
56
|
+
33139: "https://apechain.bridge.manifoldxyz.dev/rpc"
|
|
57
|
+
};
|
|
58
|
+
var CHAINS = {
|
|
59
|
+
[mainnet.id]: mainnet,
|
|
60
|
+
[sepolia.id]: sepolia,
|
|
61
|
+
[base.id]: base,
|
|
62
|
+
[optimism.id]: optimism,
|
|
63
|
+
[shape.id]: shape,
|
|
64
|
+
[apeChain.id]: apeChain
|
|
65
|
+
};
|
|
66
|
+
var TRANSPORT_TIMEOUT_MS = 8e3;
|
|
67
|
+
function getChain(chainId) {
|
|
68
|
+
const chain = CHAINS[chainId];
|
|
69
|
+
if (!chain) {
|
|
70
|
+
throw new MintError("INVALID_CONFIG", `chainId ${String(chainId)} is not supported`, { field: "chainId" });
|
|
71
|
+
}
|
|
72
|
+
return chain;
|
|
73
|
+
}
|
|
74
|
+
var TERMINAL_CODES = /* @__PURE__ */ new Set([3, -32602, -32003, 4001, 4100, 4200, 4900, 4901, 4902]);
|
|
75
|
+
var FALLBACK_CODES = /* @__PURE__ */ new Set([-32700, -32600, -32601, -32001, -32002, -32004, -32005, 429]);
|
|
76
|
+
var EXECUTION_WORDING = /execution reverted|execution error|\breverted?\b|out of gas|invalid opcode|insufficient funds|nonce too low|gas required exceeds/i;
|
|
77
|
+
var UPSTREAM_WORDING = /\btimeout\b|\btimed out\b|\btoo many requests\b|\brate limit\b|\brequest limit reached\b|\b(?:exceeded.*capacity|capacity.*exceeded)\b|\bheader not found\b|\binternal (?:json-rpc )?(?:server )?error\b|\bnon-200 status code:\s*['"]?503\b/i;
|
|
78
|
+
var TRANSPORT_ERROR_NAMES = /* @__PURE__ */ new Set(["HttpRequestError", "TimeoutError", "SocketClosedError", "WebSocketRequestError"]);
|
|
79
|
+
function carriesExecutionEvidence(value, seen = /* @__PURE__ */ new Set()) {
|
|
80
|
+
if (typeof value === "string") return /^(?:Reverted\s+)?0x(?:[a-f\d]{2})*$/i.test(value) || EXECUTION_WORDING.test(value);
|
|
81
|
+
if (!value || typeof value !== "object" || seen.has(value)) return false;
|
|
82
|
+
seen.add(value);
|
|
83
|
+
const e = value;
|
|
84
|
+
if (typeof e.code === "number" && TERMINAL_CODES.has(e.code)) return true;
|
|
85
|
+
if (typeof e.message === "string" && EXECUTION_WORDING.test(e.message)) return true;
|
|
86
|
+
return ["data", "error", "originalError", "cause", "result"].some((key) => carriesExecutionEvidence(e[key], seen));
|
|
87
|
+
}
|
|
88
|
+
function shouldFallback(err) {
|
|
89
|
+
let retry = false;
|
|
90
|
+
const seen = /* @__PURE__ */ new Set();
|
|
91
|
+
for (let e = err; e && typeof e === "object" && !seen.has(e); ) {
|
|
92
|
+
seen.add(e);
|
|
93
|
+
const error = e;
|
|
94
|
+
const message = typeof error.details === "string" ? error.details : typeof error.shortMessage !== "string" ? error.message : "";
|
|
95
|
+
if (typeof message === "string" && EXECUTION_WORDING.test(message)) return false;
|
|
96
|
+
if (carriesExecutionEvidence(error.data)) return false;
|
|
97
|
+
if (typeof error.code === "number" && TERMINAL_CODES.has(error.code)) return false;
|
|
98
|
+
if (error.name === "ContractFunctionRevertedError" || error.name === "ExecutionRevertedError") return false;
|
|
99
|
+
if (typeof error.name === "string" && TRANSPORT_ERROR_NAMES.has(error.name)) retry = true;
|
|
100
|
+
if (typeof error.code === "number" && FALLBACK_CODES.has(error.code)) retry = true;
|
|
101
|
+
if (typeof message === "string" && UPSTREAM_WORDING.test(message)) retry = true;
|
|
102
|
+
e = error.cause;
|
|
103
|
+
}
|
|
104
|
+
return retry;
|
|
105
|
+
}
|
|
106
|
+
function createReadClient(opts) {
|
|
107
|
+
const chain = getChain(opts.chainId);
|
|
108
|
+
const urls = [opts.rpcUrls?.[opts.chainId], opts.useBridge ? BRIDGE_URLS[opts.chainId] : void 0, chain.rpcUrls.default.http[0]].filter(
|
|
109
|
+
(u, i, all) => !!u && all.indexOf(u) === i
|
|
110
|
+
);
|
|
111
|
+
const transport = ({ chain: c, pollingInterval }) => {
|
|
112
|
+
const transports = urls.map((url) => http(url, { timeout: TRANSPORT_TIMEOUT_MS, retryCount: 0 })({ chain: c, pollingInterval, retryCount: 0, timeout: TRANSPORT_TIMEOUT_MS }));
|
|
113
|
+
const request = (async (args) => {
|
|
114
|
+
let last;
|
|
115
|
+
for (const t of transports) {
|
|
116
|
+
try {
|
|
117
|
+
return await t.request(args);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (!shouldFallback(err)) throw err;
|
|
120
|
+
last = err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
console.warn(`dropkit: all RPC endpoints failed for chain ${String(opts.chainId)}; pass rpcUrls={{ ${String(opts.chainId)}: '<your RPC url>' }} to <MintButton>.`);
|
|
124
|
+
throw new MintError("RPC_ERROR", `All RPC endpoints failed for chain ${String(opts.chainId)}`, { cause: last });
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
config: { key: "dropkit-fallback", name: "Dropkit fallback", request, retryCount: 0, retryDelay: 0, timeout: TRANSPORT_TIMEOUT_MS, type: "fallback" },
|
|
128
|
+
request,
|
|
129
|
+
value: {}
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
return createPublicClient({ chain, transport });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/core/wallet/index.ts
|
|
136
|
+
import { createWalletClient, custom, getAddress } from "viem";
|
|
137
|
+
var CONNECT_WIDGET_VERSION = "6.4.0";
|
|
138
|
+
var CONNECT_BASE_URL = `https://connect.manifoldxyz.dev/${CONNECT_WIDGET_VERSION}`;
|
|
139
|
+
var CONNECT_ID = "mint-button";
|
|
140
|
+
var ADDRESS_CHANGED = "ethereum-address-changed";
|
|
141
|
+
var CHAIN_CHANGED = "frontend-provider-chain-changed";
|
|
142
|
+
var LOADER_KEY = "__manifoldMintConnectLoader";
|
|
143
|
+
var STATE_KEY = "__manifoldMintConnectStateV1";
|
|
144
|
+
var win = () => window;
|
|
145
|
+
var pageState = () => win()[STATE_KEY] ??= {};
|
|
146
|
+
var TOO_OLD = "A Manifold connect widget older than 6.4.0 is already on this page; update it or remove it so dropkit can load 6.4.0.";
|
|
147
|
+
var MANIFOLD_HOST = /(^|\.)(manifoldxyz\.dev|manifold\.xyz)$/;
|
|
148
|
+
function isManifoldConnectScript(src) {
|
|
149
|
+
try {
|
|
150
|
+
const url = new URL(src, document.baseURI);
|
|
151
|
+
return url.hostname === "connect.manifoldxyz.dev" || MANIFOLD_HOST.test(url.hostname) && /\/connect\.umd[^/]*\.js$/.test(url.pathname);
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function applyState(state) {
|
|
157
|
+
if (!state.div) return;
|
|
158
|
+
const network = state.networks.join(",");
|
|
159
|
+
if (state.div.dataset.network !== network) state.div.dataset.network = network;
|
|
160
|
+
if (state.projectId && state.div.dataset.walletConnectProjectId !== state.projectId) state.div.dataset.walletConnectProjectId = state.projectId;
|
|
161
|
+
}
|
|
162
|
+
function register(state, opts) {
|
|
163
|
+
for (const id2 of opts.chainIds) if (!state.networks.includes(id2)) state.networks.push(id2);
|
|
164
|
+
const id = opts.walletConnectProjectId;
|
|
165
|
+
if (id) {
|
|
166
|
+
if (!state.projectId) state.projectId = id;
|
|
167
|
+
else if (state.projectId !== id) {
|
|
168
|
+
console.warn(`dropkit: walletConnectProjectId "${id}" ignored; this page already uses "${state.projectId}" for the shared connect widget.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
applyState(state);
|
|
172
|
+
}
|
|
173
|
+
function startLoad(state) {
|
|
174
|
+
const connect = win().ManifoldConnect;
|
|
175
|
+
if (connect?.capabilities?.includes("open")) return Promise.resolve(connect.ready).then(() => void 0);
|
|
176
|
+
const hasScript = Array.from(document.querySelectorAll("script[src]")).some((s) => isManifoldConnectScript(s.src));
|
|
177
|
+
if (hasScript || document.querySelector('[data-widget="m-connect"]')) {
|
|
178
|
+
return Promise.reject(new MintError("CONNECT_WIDGET_TOO_OLD", TOO_OLD));
|
|
179
|
+
}
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const css = document.createElement("link");
|
|
182
|
+
css.rel = "stylesheet";
|
|
183
|
+
css.href = `${CONNECT_BASE_URL}/connect.css`;
|
|
184
|
+
document.head.appendChild(css);
|
|
185
|
+
state.nodes.push(css);
|
|
186
|
+
const div = document.createElement("div");
|
|
187
|
+
div.hidden = true;
|
|
188
|
+
div.style.display = "none";
|
|
189
|
+
div.dataset.widget = "m-connect";
|
|
190
|
+
div.dataset.connectId = CONNECT_ID;
|
|
191
|
+
div.dataset.autoReconnect = "true";
|
|
192
|
+
div.dataset.enableNetworkSwitching = "true";
|
|
193
|
+
state.div = div;
|
|
194
|
+
applyState(state);
|
|
195
|
+
document.body.appendChild(div);
|
|
196
|
+
state.nodes.push(div);
|
|
197
|
+
const script = document.createElement("script");
|
|
198
|
+
script.src = `${CONNECT_BASE_URL}/connect.umd.min.js`;
|
|
199
|
+
script.async = true;
|
|
200
|
+
script.addEventListener("load", () => {
|
|
201
|
+
const ready = win().ManifoldConnect?.ready;
|
|
202
|
+
if (!ready) return reject(new MintError("CONNECT_WIDGET_TOO_OLD", TOO_OLD));
|
|
203
|
+
Promise.resolve(ready).then(() => resolve(), reject);
|
|
204
|
+
});
|
|
205
|
+
script.addEventListener("error", () => reject(new MintError("RPC_ERROR", `Could not load the Manifold connect widget from ${CONNECT_BASE_URL}`)));
|
|
206
|
+
document.head.appendChild(script);
|
|
207
|
+
state.nodes.push(script);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function loadConnectWidget(opts) {
|
|
211
|
+
const w = win();
|
|
212
|
+
const page = pageState();
|
|
213
|
+
const existing = w[LOADER_KEY];
|
|
214
|
+
if (existing) {
|
|
215
|
+
if (page.load?.promise !== existing) {
|
|
216
|
+
const div = document.querySelector(`[data-widget="m-connect"][data-connect-id="${CONNECT_ID}"]`) ?? void 0;
|
|
217
|
+
page.load = {
|
|
218
|
+
promise: existing,
|
|
219
|
+
networks: div?.dataset.network?.split(",").filter(Boolean).map(Number) ?? [],
|
|
220
|
+
projectId: div?.dataset.walletConnectProjectId,
|
|
221
|
+
div,
|
|
222
|
+
nodes: []
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
register(page.load, opts);
|
|
226
|
+
return existing;
|
|
227
|
+
}
|
|
228
|
+
let resolve;
|
|
229
|
+
let reject;
|
|
230
|
+
const p = new Promise((res, rej) => {
|
|
231
|
+
resolve = res;
|
|
232
|
+
reject = rej;
|
|
233
|
+
});
|
|
234
|
+
const state = { promise: p, networks: [], nodes: [] };
|
|
235
|
+
page.load = state;
|
|
236
|
+
w[LOADER_KEY] = p;
|
|
237
|
+
p.catch(() => {
|
|
238
|
+
if (w[LOADER_KEY] === p) delete w[LOADER_KEY];
|
|
239
|
+
if (page.load === state) page.load = void 0;
|
|
240
|
+
for (const node of state.nodes) node.parentNode?.removeChild(node);
|
|
241
|
+
state.nodes.length = 0;
|
|
242
|
+
state.div = void 0;
|
|
243
|
+
});
|
|
244
|
+
register(state, opts);
|
|
245
|
+
try {
|
|
246
|
+
startLoad(state).then(resolve, reject);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
reject(error);
|
|
249
|
+
}
|
|
250
|
+
return p;
|
|
251
|
+
}
|
|
252
|
+
function openConnectModal() {
|
|
253
|
+
const page = pageState();
|
|
254
|
+
if (page.pendingModal) return page.pendingModal;
|
|
255
|
+
const p = new Promise((resolve) => {
|
|
256
|
+
const onClosed = (e) => {
|
|
257
|
+
const detail = e.detail ?? {};
|
|
258
|
+
if (detail.connectId !== void 0 && detail.connectId !== CONNECT_ID) return;
|
|
259
|
+
window.removeEventListener("m-connect:closed", onClosed);
|
|
260
|
+
page.pendingModal = void 0;
|
|
261
|
+
resolve(detail.reason === "connected" ? "connected" : "dismissed");
|
|
262
|
+
};
|
|
263
|
+
window.addEventListener("m-connect:closed", onClosed);
|
|
264
|
+
});
|
|
265
|
+
page.pendingModal = p;
|
|
266
|
+
window.dispatchEvent(new CustomEvent("m-connect:open", { detail: { connectId: CONNECT_ID } }));
|
|
267
|
+
return p;
|
|
268
|
+
}
|
|
269
|
+
function getAccount() {
|
|
270
|
+
const p = win().ManifoldEthereumProvider;
|
|
271
|
+
const address = p?.selectedAddress?.();
|
|
272
|
+
let chainId = p?.chainId?.();
|
|
273
|
+
if (chainId === void 0 || chainId === null) {
|
|
274
|
+
try {
|
|
275
|
+
chainId = p?.browserChainId?.();
|
|
276
|
+
} catch {
|
|
277
|
+
chainId = void 0;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!address || chainId === void 0 || chainId === null) return null;
|
|
281
|
+
return { address: getAddress(address), chainId: Number(chainId) };
|
|
282
|
+
}
|
|
283
|
+
function subscribeAccount(cb) {
|
|
284
|
+
const handler = () => cb(getAccount());
|
|
285
|
+
window.addEventListener(ADDRESS_CHANGED, handler);
|
|
286
|
+
window.addEventListener(CHAIN_CHANGED, handler);
|
|
287
|
+
return () => {
|
|
288
|
+
window.removeEventListener(ADDRESS_CHANGED, handler);
|
|
289
|
+
window.removeEventListener(CHAIN_CHANGED, handler);
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function eip1193() {
|
|
293
|
+
const p = win().ManifoldEthereumProvider;
|
|
294
|
+
const provider = (p?.provider?.(true) ?? p?.browserProvider?.())?.provider;
|
|
295
|
+
if (!provider) throw new MintError("CHAIN_SWITCH_FAILED", "No wallet is connected.");
|
|
296
|
+
return provider;
|
|
297
|
+
}
|
|
298
|
+
function getWalletClient(chainId) {
|
|
299
|
+
const provider = eip1193();
|
|
300
|
+
const request = ((args) => provider.request(args));
|
|
301
|
+
return createWalletClient({ chain: getChain(chainId), transport: custom({ request }) });
|
|
302
|
+
}
|
|
303
|
+
var code = (e) => e?.code;
|
|
304
|
+
async function switchToChain(chainId) {
|
|
305
|
+
const chain = getChain(chainId);
|
|
306
|
+
const provider = eip1193();
|
|
307
|
+
const hexId = `0x${chainId.toString(16)}`;
|
|
308
|
+
const switchChain = () => provider.request({ method: "wallet_switchEthereumChain", params: [{ chainId: hexId }] });
|
|
309
|
+
try {
|
|
310
|
+
try {
|
|
311
|
+
await switchChain();
|
|
312
|
+
} catch (e) {
|
|
313
|
+
if (code(e) !== 4902) throw e;
|
|
314
|
+
await provider.request({
|
|
315
|
+
method: "wallet_addEthereumChain",
|
|
316
|
+
params: [
|
|
317
|
+
{
|
|
318
|
+
chainId: hexId,
|
|
319
|
+
chainName: chain.name,
|
|
320
|
+
nativeCurrency: chain.nativeCurrency,
|
|
321
|
+
rpcUrls: [...chain.rpcUrls.default.http],
|
|
322
|
+
...chain.blockExplorers ? { blockExplorerUrls: [chain.blockExplorers.default.url] } : {}
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
});
|
|
326
|
+
await switchChain();
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {
|
|
329
|
+
if (code(e) === 4001) throw new MintError("USER_REJECTED", "You rejected the network switch.", { cause: e });
|
|
330
|
+
throw new MintError("CHAIN_SWITCH_FAILED", `Could not switch the wallet to chain ${String(chainId)}.`, { cause: e });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/core/adapters/manifold-claim/index.ts
|
|
335
|
+
import {
|
|
336
|
+
BaseError,
|
|
337
|
+
ContractFunctionRevertedError,
|
|
338
|
+
decodeErrorResult,
|
|
339
|
+
encodeFunctionData,
|
|
340
|
+
formatUnits,
|
|
341
|
+
getAddress as getAddress2,
|
|
342
|
+
isAddress,
|
|
343
|
+
isHex,
|
|
344
|
+
zeroAddress,
|
|
345
|
+
zeroHash
|
|
346
|
+
} from "viem";
|
|
347
|
+
|
|
348
|
+
// src/core/adapters/manifold-claim/abis.ts
|
|
349
|
+
import { parseAbi } from "viem";
|
|
350
|
+
var CLAIM_721_V5_ABI = parseAbi([
|
|
351
|
+
"function getClaim(address creatorContractAddress, uint256 instanceId) view returns ((uint32 total, uint32 totalMax, uint32 walletMax, uint48 startDate, uint48 endDate, uint8 storageProtocol, uint8 contractVersion, bool identical, bytes32 merkleRoot, string location, uint256 cost, address paymentReceiver, address erc20) claim)"
|
|
352
|
+
]);
|
|
353
|
+
var CLAIM_1155_V5_ABI = parseAbi([
|
|
354
|
+
"function getClaim(address creatorContractAddress, uint256 instanceId) view returns ((uint32 total, uint32 totalMax, uint32 walletMax, uint48 startDate, uint48 endDate, uint8 storageProtocol, bytes32 merkleRoot, string location, uint256 tokenId, uint256 cost, address paymentReceiver, address erc20) claim)"
|
|
355
|
+
]);
|
|
356
|
+
var CLAIM_721_V7_ABI = parseAbi([
|
|
357
|
+
"function getClaim(address creatorContractAddress, uint256 instanceId) view returns ((uint32 total, uint32 totalMax, uint32 walletMax, uint48 startDate, uint48 endDate, uint8 storageProtocol, uint8 contractVersion, bool identical, bytes32 merkleRoot, string location, uint256 cost, address paymentReceiver, address erc20, address signingAddress) claim)"
|
|
358
|
+
]);
|
|
359
|
+
var CLAIM_1155_V7_ABI = parseAbi([
|
|
360
|
+
"function getClaim(address creatorContractAddress, uint256 instanceId) view returns ((uint32 total, uint32 totalMax, uint32 walletMax, uint48 startDate, uint48 endDate, uint8 storageProtocol, bytes32 merkleRoot, string location, uint256 tokenId, uint256 cost, address paymentReceiver, address erc20, address signingAddress) claim)"
|
|
361
|
+
]);
|
|
362
|
+
var EXTENSION_COMMON_ABI = parseAbi([
|
|
363
|
+
"function MINT_FEE() view returns (uint256)",
|
|
364
|
+
"function MINT_FEE_MERKLE() view returns (uint256)",
|
|
365
|
+
"function getTotalMints(address minter, address creatorContractAddress, uint256 instanceId) view returns (uint32)",
|
|
366
|
+
"function checkMintIndices(address creatorContractAddress, uint256 instanceId, uint32[] mintIndices) view returns (bool[] minted)",
|
|
367
|
+
"function mintProxy(address creatorContractAddress, uint256 instanceId, uint16 mintCount, uint32[] mintIndices, bytes32[][] merkleProofs, address mintFor) payable"
|
|
368
|
+
]);
|
|
369
|
+
var EXTENSION_ERRORS_ABI = parseAbi([
|
|
370
|
+
"error Error(string message)",
|
|
371
|
+
"error Panic(uint256 code)",
|
|
372
|
+
"error ClaimInactive()",
|
|
373
|
+
"error ClaimNotInitialized()",
|
|
374
|
+
"error ExpiredSignature()",
|
|
375
|
+
"error FailedToTransfer()",
|
|
376
|
+
"error InvalidAirdrop()",
|
|
377
|
+
"error InvalidInput()",
|
|
378
|
+
"error InvalidInstance()",
|
|
379
|
+
"error InvalidSignature()",
|
|
380
|
+
"error InvalidStartDate()",
|
|
381
|
+
"error InvalidStorageProtocol()",
|
|
382
|
+
"error MustUseSignatureMinting()",
|
|
383
|
+
"error TokenDNE()",
|
|
384
|
+
"error TooManyRequested()"
|
|
385
|
+
]);
|
|
386
|
+
var CREATOR_ABI = parseAbi(["function getExtensions() view returns (address[])"]);
|
|
387
|
+
var ERC20_ABI = parseAbi([
|
|
388
|
+
"function symbol() view returns (string)",
|
|
389
|
+
"function decimals() view returns (uint8)",
|
|
390
|
+
"function balanceOf(address owner) view returns (uint256)",
|
|
391
|
+
"function allowance(address owner, address spender) view returns (uint256)",
|
|
392
|
+
"function approve(address spender, uint256 amount) returns (bool)"
|
|
393
|
+
]);
|
|
394
|
+
|
|
395
|
+
// src/core/adapters/manifold-claim/merkle.ts
|
|
396
|
+
var UINT32_MAX = 4294967295;
|
|
397
|
+
async function getJson(fetchFn, url, signal, what) {
|
|
398
|
+
let res;
|
|
399
|
+
try {
|
|
400
|
+
signal?.throwIfAborted();
|
|
401
|
+
res = await fetchFn(url, signal ? { signal } : void 0);
|
|
402
|
+
} catch (cause) {
|
|
403
|
+
throw new MintError("ALLOWLIST_UNAVAILABLE", `Could not reach the allowlist service (${what}).`, { cause });
|
|
404
|
+
}
|
|
405
|
+
if (res.status === 429) throw new MintError("RATE_LIMITED", "The allowlist service is rate limiting requests. Try again shortly.");
|
|
406
|
+
if (!res.ok) throw new MintError("ALLOWLIST_UNAVAILABLE", `The allowlist service has no data for this claim (${what}: HTTP ${res.status}).`);
|
|
407
|
+
try {
|
|
408
|
+
return await res.json();
|
|
409
|
+
} catch (cause) {
|
|
410
|
+
throw new MintError("ALLOWLIST_UNAVAILABLE", `The allowlist service returned an unreadable response (${what}).`, { cause });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
var isRecord = (v) => typeof v === "object" && v !== null;
|
|
414
|
+
var scopeCaches = /* @__PURE__ */ new WeakMap();
|
|
415
|
+
function memo(cache, key, signal, load) {
|
|
416
|
+
const hit = cache.get(key);
|
|
417
|
+
if (hit) return hit;
|
|
418
|
+
const p = withSignal(load(), signal);
|
|
419
|
+
cache.set(key, p);
|
|
420
|
+
p.catch(() => {
|
|
421
|
+
if (cache.get(key) === p) cache.delete(key);
|
|
422
|
+
});
|
|
423
|
+
return p;
|
|
424
|
+
}
|
|
425
|
+
function withSignal(p, signal) {
|
|
426
|
+
if (!signal) return p;
|
|
427
|
+
return new Promise((resolve, reject) => {
|
|
428
|
+
const onAbort = () => reject(new MintError("ALLOWLIST_UNAVAILABLE", "The allowlist request was cancelled.", { cause: signal.reason }));
|
|
429
|
+
p.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
430
|
+
if (signal.aborted) return onAbort();
|
|
431
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
async function loadInstance(fetchFn, api, instanceId, signal) {
|
|
435
|
+
const instance = await getJson(fetchFn, `${api}/public/instance/data?id=${instanceId}`, signal, "instance data");
|
|
436
|
+
const publicData = isRecord(instance) ? instance.publicData : void 0;
|
|
437
|
+
const allowlist = isRecord(publicData) ? publicData.instanceAllowlist : void 0;
|
|
438
|
+
const merkleTreeId = isRecord(allowlist) ? allowlist.merkleTreeId : void 0;
|
|
439
|
+
const appId = isRecord(instance) ? instance.appId : void 0;
|
|
440
|
+
if (typeof merkleTreeId !== "number" && typeof merkleTreeId !== "string" || typeof appId !== "number" && typeof appId !== "string") {
|
|
441
|
+
throw new MintError("ALLOWLIST_UNAVAILABLE", "The allowlist for this claim is not published yet.");
|
|
442
|
+
}
|
|
443
|
+
return { merkleTreeId, appId };
|
|
444
|
+
}
|
|
445
|
+
async function loadEntries(fetchFn, api, instanceId, recipient, scoped) {
|
|
446
|
+
const { signal } = scoped;
|
|
447
|
+
const { merkleTreeId, appId } = await memo(scoped.instances, `${api}|${instanceId}`, signal, () => loadInstance(fetchFn, api, instanceId, signal));
|
|
448
|
+
const url = `${api}/public/merkleTree/${encodeURIComponent(String(merkleTreeId))}/merkleInfo?address=${recipient}&appId=${encodeURIComponent(String(appId))}`;
|
|
449
|
+
const body = await getJson(fetchFn, url, signal, "merkle info");
|
|
450
|
+
if (!Array.isArray(body)) throw new MintError("ALLOWLIST_UNAVAILABLE", "The allowlist service returned an unexpected response.");
|
|
451
|
+
return body.map((row) => {
|
|
452
|
+
const proof = isRecord(row) ? row.merkleProof : void 0;
|
|
453
|
+
const value = isRecord(row) ? row.value : void 0;
|
|
454
|
+
if (!Array.isArray(proof) || !proof.every((p) => typeof p === "string" && /^0x[0-9a-fA-F]{64}$/.test(p)) || typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > UINT32_MAX) {
|
|
455
|
+
throw new MintError("ALLOWLIST_UNAVAILABLE", "The allowlist service returned an unexpected response.");
|
|
456
|
+
}
|
|
457
|
+
return { mintIndex: value, proof };
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
async function fetchMerkleEntries(args) {
|
|
461
|
+
if (args.signal?.aborted) {
|
|
462
|
+
throw new MintError("ALLOWLIST_UNAVAILABLE", "The allowlist request was cancelled.", { cause: args.signal.reason });
|
|
463
|
+
}
|
|
464
|
+
const fetchFn = args.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
465
|
+
const key = `${args.api}|${args.instanceId}|${args.recipient.toLowerCase()}`;
|
|
466
|
+
let scoped = scopeCaches.get(args.scope);
|
|
467
|
+
if (!scoped || scoped.signal?.aborted) {
|
|
468
|
+
scoped = { signal: args.signal, instances: /* @__PURE__ */ new Map(), entries: /* @__PURE__ */ new Map() };
|
|
469
|
+
scopeCaches.set(args.scope, scoped);
|
|
470
|
+
}
|
|
471
|
+
const cache = scoped;
|
|
472
|
+
const entries = await memo(cache.entries, key, cache.signal, () => loadEntries(fetchFn, args.api, args.instanceId, args.recipient, cache));
|
|
473
|
+
return entries.slice();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/core/adapters/manifold-claim/index.ts
|
|
477
|
+
var V6_721 = "0x1eb73fee2090fb1c20105d5ba887e3c3ba14a17e";
|
|
478
|
+
var V6_1155 = "0x04ba6cf3c5aa6d4946f5b7f7adf111012a9fac65";
|
|
479
|
+
var V7_721 = "0x23aa05a271debffaa3d75739af5581f744b326e4";
|
|
480
|
+
var V7_1155 = "0x26bbea7803dcac346d5f5f135b57cf2c752a02be";
|
|
481
|
+
var V8_721 = "0x5889e848b2423344f7969bc158100c07595c0137";
|
|
482
|
+
var V8_1155 = "0x88438f4d4b8516e758a319ad16924da60e1e56cc";
|
|
483
|
+
var v6 = (chainId) => [
|
|
484
|
+
{ chainId, address: V6_721, generation: "V6", spec: "721", abi: "Claim721V5Optimization" },
|
|
485
|
+
{ chainId, address: V6_1155, generation: "V6", spec: "1155", abi: "Claim1155V5Optimization" }
|
|
486
|
+
];
|
|
487
|
+
var v7 = (chainId) => [
|
|
488
|
+
{ chainId, address: V7_721, generation: "V7", spec: "721", abi: "Claim721V7" },
|
|
489
|
+
{ chainId, address: V7_1155, generation: "V7", spec: "1155", abi: "Claim1155V7" }
|
|
490
|
+
];
|
|
491
|
+
var EXTENSIONS = [
|
|
492
|
+
...v6(1),
|
|
493
|
+
...v7(1),
|
|
494
|
+
...v6(8453),
|
|
495
|
+
...v7(8453),
|
|
496
|
+
...v6(10),
|
|
497
|
+
...v7(10),
|
|
498
|
+
...v7(360),
|
|
499
|
+
{ chainId: 360, address: V8_1155, generation: "V8", spec: "1155", abi: "Claim1155V7" },
|
|
500
|
+
...v6(11155111),
|
|
501
|
+
...v7(11155111),
|
|
502
|
+
{ chainId: 11155111, address: V8_721, generation: "V8", spec: "721", abi: "Claim721V7" },
|
|
503
|
+
{ chainId: 11155111, address: V8_1155, generation: "V8", spec: "1155", abi: "Claim1155V7" }
|
|
504
|
+
];
|
|
505
|
+
function findExtension(chainId, address) {
|
|
506
|
+
const a = address.toLowerCase();
|
|
507
|
+
return EXTENSIONS.find((e) => e.chainId === chainId && e.address === a);
|
|
508
|
+
}
|
|
509
|
+
var MERKLE_API = "https://apps.api.manifoldxyz.dev";
|
|
510
|
+
var MAX_MINT_COUNT = 65535;
|
|
511
|
+
var GAS_UNITS_BASE = 100000n;
|
|
512
|
+
var GAS_UNITS_PER_TOKEN = 150000n;
|
|
513
|
+
var GAS_UNITS_APPROVE = 60000n;
|
|
514
|
+
var dropResolved = /* @__PURE__ */ new WeakMap();
|
|
515
|
+
var WALLET_DATA_BRAND = /* @__PURE__ */ Symbol("manifold-claim-wallet-data");
|
|
516
|
+
var isWalletData = (d) => typeof d === "object" && d !== null && d.brand === WALLET_DATA_BRAND;
|
|
517
|
+
var isAllowlist = (claim) => claim.merkleRoot !== zeroHash;
|
|
518
|
+
function isContractFailure(e) {
|
|
519
|
+
if (!(e instanceof BaseError)) return false;
|
|
520
|
+
return e.walk((x) => x instanceof ContractFunctionRevertedError || x.name === "ContractFunctionZeroDataError") !== null;
|
|
521
|
+
}
|
|
522
|
+
function toRpcError(e) {
|
|
523
|
+
if (e instanceof MintError) return e;
|
|
524
|
+
const msg = e instanceof BaseError ? e.shortMessage : e instanceof Error ? e.message : String(e);
|
|
525
|
+
return new MintError("RPC_ERROR", `RPC request failed: ${msg}`, { cause: e });
|
|
526
|
+
}
|
|
527
|
+
async function rpc(fn) {
|
|
528
|
+
try {
|
|
529
|
+
return await fn();
|
|
530
|
+
} catch (e) {
|
|
531
|
+
throw toRpcError(e);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function revertData(e) {
|
|
535
|
+
const hit = e.walk((x) => {
|
|
536
|
+
const d2 = x.data;
|
|
537
|
+
return isHex(d2) && d2.length >= 10 || typeof d2 === "object" && d2 !== null && isHex(d2.data);
|
|
538
|
+
});
|
|
539
|
+
const d = hit?.data;
|
|
540
|
+
if (isHex(d)) return d;
|
|
541
|
+
const inner = d?.data;
|
|
542
|
+
return isHex(inner) ? inner : void 0;
|
|
543
|
+
}
|
|
544
|
+
function decodeRevert(e) {
|
|
545
|
+
if (!(e instanceof BaseError)) return null;
|
|
546
|
+
const data = revertData(e);
|
|
547
|
+
if (data) {
|
|
548
|
+
try {
|
|
549
|
+
const d = decodeErrorResult({ abi: EXTENSION_ERRORS_ABI, data });
|
|
550
|
+
if (d.errorName === "Error" && typeof d.args?.[0] === "string") return d.args[0];
|
|
551
|
+
if (d.errorName === "Panic") return `Panic(${String(d.args?.[0])})`;
|
|
552
|
+
return d.errorName;
|
|
553
|
+
} catch {
|
|
554
|
+
return `reverted with unknown error ${data.slice(0, 10)}`;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const named = e.walk((x) => ["ExecutionRevertedError", "InsufficientFundsError"].includes(x.name));
|
|
558
|
+
return named instanceof BaseError ? named.shortMessage : null;
|
|
559
|
+
}
|
|
560
|
+
var money = (value, decimals, symbol) => ({ value, decimals, symbol, formatted: formatUnits(value, decimals) });
|
|
561
|
+
function timestamp(v, field) {
|
|
562
|
+
if (v === 0n) return null;
|
|
563
|
+
if (v > BigInt(Number.MAX_SAFE_INTEGER)) throw new MintError("RPC_ERROR", `Claim ${field} does not fit a safe integer`);
|
|
564
|
+
return Number(v);
|
|
565
|
+
}
|
|
566
|
+
function parseConfig(config) {
|
|
567
|
+
if (!isAddress(config.creatorContract, { strict: false })) {
|
|
568
|
+
throw new MintError("INVALID_CONFIG", "creatorContract is not a valid address.", { field: "creatorContract" });
|
|
569
|
+
}
|
|
570
|
+
if (config.extension !== void 0 && !isAddress(config.extension, { strict: false })) {
|
|
571
|
+
throw new MintError("INVALID_CONFIG", "extension is not a valid address.", { field: "extension" });
|
|
572
|
+
}
|
|
573
|
+
if (typeof config.instanceId !== "string" || !/^\d+$/.test(config.instanceId)) {
|
|
574
|
+
throw new MintError("INVALID_CONFIG", "instanceId must be a non-negative integer string.", { field: "instanceId" });
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
creator: getAddress2(config.creatorContract),
|
|
578
|
+
instanceId: BigInt(config.instanceId),
|
|
579
|
+
extension: config.extension === void 0 ? void 0 : getAddress2(config.extension)
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
var notFound = (config) => new MintError("CLAIM_NOT_FOUND", `No Manifold claim ${config.instanceId} found on ${config.creatorContract}.`);
|
|
583
|
+
async function readClaim(client, entry, creator, instanceId, config) {
|
|
584
|
+
const address = getAddress2(entry.address);
|
|
585
|
+
const args = [creator, instanceId];
|
|
586
|
+
try {
|
|
587
|
+
switch (entry.abi) {
|
|
588
|
+
case "Claim721V5Optimization": {
|
|
589
|
+
const c = await client.readContract({ address, abi: CLAIM_721_V5_ABI, functionName: "getClaim", args });
|
|
590
|
+
return { ...base2(c), signingAddress: null };
|
|
591
|
+
}
|
|
592
|
+
case "Claim1155V5Optimization": {
|
|
593
|
+
const c = await client.readContract({ address, abi: CLAIM_1155_V5_ABI, functionName: "getClaim", args });
|
|
594
|
+
return { ...base2(c), signingAddress: null };
|
|
595
|
+
}
|
|
596
|
+
case "Claim721V7": {
|
|
597
|
+
const c = await client.readContract({ address, abi: CLAIM_721_V7_ABI, functionName: "getClaim", args });
|
|
598
|
+
return { ...base2(c), signingAddress: c.signingAddress };
|
|
599
|
+
}
|
|
600
|
+
case "Claim1155V7": {
|
|
601
|
+
const c = await client.readContract({ address, abi: CLAIM_1155_V7_ABI, functionName: "getClaim", args });
|
|
602
|
+
return { ...base2(c), signingAddress: c.signingAddress };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} catch (e) {
|
|
606
|
+
if (isContractFailure(e)) throw notFound(config);
|
|
607
|
+
throw toRpcError(e);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function base2(c) {
|
|
611
|
+
return {
|
|
612
|
+
total: BigInt(c.total),
|
|
613
|
+
totalMax: BigInt(c.totalMax),
|
|
614
|
+
walletMax: BigInt(c.walletMax),
|
|
615
|
+
startDate: BigInt(c.startDate),
|
|
616
|
+
endDate: BigInt(c.endDate),
|
|
617
|
+
merkleRoot: c.merkleRoot,
|
|
618
|
+
cost: c.cost,
|
|
619
|
+
erc20: c.erc20
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
async function hasCode(client, address) {
|
|
623
|
+
const code2 = await rpc(() => client.getCode({ address: getAddress2(address) }));
|
|
624
|
+
return code2 !== void 0 && code2 !== "0x";
|
|
625
|
+
}
|
|
626
|
+
var unknownExtension = (address, why) => new MintError("UNKNOWN_EXTENSION", `Extension ${address} ${why}.`);
|
|
627
|
+
async function resolveExtension(ctx, config) {
|
|
628
|
+
const { creator, instanceId, extension } = parseConfig(config);
|
|
629
|
+
const { client } = ctx;
|
|
630
|
+
if (extension) {
|
|
631
|
+
const entry = findExtension(config.chainId, extension);
|
|
632
|
+
if (!entry) throw unknownExtension(extension, `is not a supported Manifold claim extension on chain ${config.chainId}`);
|
|
633
|
+
if (!await hasCode(client, extension)) throw unknownExtension(extension, "has no contract code on this chain");
|
|
634
|
+
return { entry, claim: await readClaim(client, entry, creator, instanceId, config) };
|
|
635
|
+
}
|
|
636
|
+
let registered;
|
|
637
|
+
try {
|
|
638
|
+
registered = await client.readContract({ address: creator, abi: CREATOR_ABI, functionName: "getExtensions" });
|
|
639
|
+
} catch (e) {
|
|
640
|
+
if (isContractFailure(e)) throw notFound(config);
|
|
641
|
+
throw toRpcError(e);
|
|
642
|
+
}
|
|
643
|
+
const candidates = [...new Set(registered.map((a) => a.toLowerCase()))].map((a) => findExtension(config.chainId, a)).filter((e) => e !== void 0);
|
|
644
|
+
const found = await Promise.all(
|
|
645
|
+
candidates.map(async (entry) => {
|
|
646
|
+
if (!await hasCode(client, entry.address)) return null;
|
|
647
|
+
try {
|
|
648
|
+
return { entry, claim: await readClaim(client, entry, creator, instanceId, config) };
|
|
649
|
+
} catch (e) {
|
|
650
|
+
if (e instanceof MintError && e.reason === "CLAIM_NOT_FOUND") return null;
|
|
651
|
+
throw e;
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
const hits = found.filter((f) => f !== null);
|
|
656
|
+
if (hits.length === 0) throw notFound(config);
|
|
657
|
+
if (hits.length > 1) {
|
|
658
|
+
throw new MintError(
|
|
659
|
+
"AMBIGUOUS_EXTENSION",
|
|
660
|
+
`Claim ${config.instanceId} exists on ${hits.length} extensions (${hits.map((h) => h.entry.address).join(", ")}). Set config.extension.`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
return hits[0];
|
|
664
|
+
}
|
|
665
|
+
async function loadDrop(ctx, config) {
|
|
666
|
+
const { entry, claim } = await resolveExtension(ctx, config);
|
|
667
|
+
if (claim.signingAddress !== null && claim.signingAddress !== zeroAddress) {
|
|
668
|
+
throw new MintError("SIGNATURE_CLAIM", "This claim mints with a signature (redemption code), which this button does not support.");
|
|
669
|
+
}
|
|
670
|
+
const { client } = ctx;
|
|
671
|
+
const extension = getAddress2(entry.address);
|
|
672
|
+
const fee = await rpc(
|
|
673
|
+
() => client.readContract({ address: extension, abi: EXTENSION_COMMON_ABI, functionName: isAllowlist(claim) ? "MINT_FEE_MERKLE" : "MINT_FEE" })
|
|
674
|
+
);
|
|
675
|
+
const native = client.chain?.nativeCurrency ?? { symbol: "ETH", decimals: 18 };
|
|
676
|
+
let unitPrice;
|
|
677
|
+
let paymentToken = null;
|
|
678
|
+
if (claim.erc20 === zeroAddress) {
|
|
679
|
+
unitPrice = money(claim.cost, native.decimals, native.symbol);
|
|
680
|
+
} else {
|
|
681
|
+
paymentToken = getAddress2(claim.erc20);
|
|
682
|
+
const token = paymentToken;
|
|
683
|
+
const [symbol, decimals] = await rpc(
|
|
684
|
+
() => Promise.all([
|
|
685
|
+
client.readContract({ address: token, abi: ERC20_ABI, functionName: "symbol" }),
|
|
686
|
+
client.readContract({ address: token, abi: ERC20_ABI, functionName: "decimals" })
|
|
687
|
+
])
|
|
688
|
+
);
|
|
689
|
+
unitPrice = money(claim.cost, decimals, symbol);
|
|
690
|
+
}
|
|
691
|
+
const drop = {
|
|
692
|
+
chainId: config.chainId,
|
|
693
|
+
unitPrice,
|
|
694
|
+
unitFee: money(fee, native.decimals, native.symbol),
|
|
695
|
+
totalMinted: claim.total,
|
|
696
|
+
maxSupply: claim.totalMax === 0n ? null : claim.totalMax,
|
|
697
|
+
startTime: timestamp(claim.startDate, "startDate"),
|
|
698
|
+
endTime: timestamp(claim.endDate, "endDate"),
|
|
699
|
+
isAllowlist: isAllowlist(claim),
|
|
700
|
+
paymentToken,
|
|
701
|
+
pricePer: "unit"
|
|
702
|
+
};
|
|
703
|
+
const resolved = { entry, claim, fee };
|
|
704
|
+
dropResolved.set(drop, resolved);
|
|
705
|
+
return { drop, resolved };
|
|
706
|
+
}
|
|
707
|
+
async function loadWallet(ctx, config, args) {
|
|
708
|
+
const { creator, instanceId } = parseConfig(config);
|
|
709
|
+
const { client } = ctx;
|
|
710
|
+
const resolved = dropResolved.get(args.drop) ?? (await loadDrop(ctx, config)).resolved;
|
|
711
|
+
const { entry, claim } = resolved;
|
|
712
|
+
const extension = getAddress2(entry.address);
|
|
713
|
+
const payer = getAddress2(args.payer);
|
|
714
|
+
const recipient = getAddress2(args.recipient);
|
|
715
|
+
let walletMinted = null;
|
|
716
|
+
let walletMax = null;
|
|
717
|
+
let allowlistRemaining = null;
|
|
718
|
+
let maxQuantity = null;
|
|
719
|
+
let ineligible = null;
|
|
720
|
+
let unused = [];
|
|
721
|
+
if (isAllowlist(claim)) {
|
|
722
|
+
const entries = await fetchMerkleEntries({ scope: ctx.cacheScope ?? args.drop, api: MERKLE_API, instanceId: config.instanceId, recipient, fetch: ctx.fetch, signal: ctx.signal });
|
|
723
|
+
if (entries.length > 0) {
|
|
724
|
+
const minted = await rpc(
|
|
725
|
+
() => client.readContract({
|
|
726
|
+
address: extension,
|
|
727
|
+
abi: EXTENSION_COMMON_ABI,
|
|
728
|
+
functionName: "checkMintIndices",
|
|
729
|
+
args: [creator, instanceId, entries.map((e) => e.mintIndex)]
|
|
730
|
+
})
|
|
731
|
+
);
|
|
732
|
+
unused = entries.filter((_, i) => minted[i] === false);
|
|
733
|
+
}
|
|
734
|
+
allowlistRemaining = unused.length;
|
|
735
|
+
maxQuantity = unused.length;
|
|
736
|
+
if (entries.length === 0) {
|
|
737
|
+
ineligible = { reason: "NOT_ON_ALLOWLIST", message: "This wallet is not on the allowlist." };
|
|
738
|
+
} else if (unused.length === 0) {
|
|
739
|
+
ineligible = { reason: "ALLOWLIST_USED", message: "This wallet has already used all its allowlist spots." };
|
|
740
|
+
}
|
|
741
|
+
} else if (claim.walletMax > 0n) {
|
|
742
|
+
walletMax = claim.walletMax;
|
|
743
|
+
walletMinted = BigInt(
|
|
744
|
+
await rpc(() => client.readContract({ address: extension, abi: EXTENSION_COMMON_ABI, functionName: "getTotalMints", args: [recipient, creator, instanceId] }))
|
|
745
|
+
);
|
|
746
|
+
const left = walletMax > walletMinted ? walletMax - walletMinted : 0n;
|
|
747
|
+
maxQuantity = Number(left);
|
|
748
|
+
if (left === 0n) ineligible = { reason: "WALLET_LIMIT", message: `This wallet has already minted its limit of ${walletMax}.` };
|
|
749
|
+
}
|
|
750
|
+
const token = claim.erc20 === zeroAddress ? null : getAddress2(claim.erc20);
|
|
751
|
+
const [native, erc20, gasPrice] = await rpc(
|
|
752
|
+
() => Promise.all([
|
|
753
|
+
client.getBalance({ address: payer }),
|
|
754
|
+
token ? client.readContract({ address: token, abi: ERC20_ABI, functionName: "balanceOf", args: [payer] }) : Promise.resolve(null),
|
|
755
|
+
client.getGasPrice()
|
|
756
|
+
])
|
|
757
|
+
);
|
|
758
|
+
const gasUnits = GAS_UNITS_BASE + GAS_UNITS_PER_TOKEN * BigInt(Math.max(1, args.quantity)) + (token ? GAS_UNITS_APPROVE : 0n);
|
|
759
|
+
const data = { brand: WALLET_DATA_BRAND, resolved, config, recipient, unused };
|
|
760
|
+
return {
|
|
761
|
+
walletMinted,
|
|
762
|
+
walletMax,
|
|
763
|
+
allowlistRemaining,
|
|
764
|
+
maxQuantity,
|
|
765
|
+
ineligible,
|
|
766
|
+
balances: { native, erc20 },
|
|
767
|
+
gasReserve: gasPrice * gasUnits,
|
|
768
|
+
data
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
var sameClaim = (a, b) => a.chainId === b.chainId && a.creatorContract.toLowerCase() === b.creatorContract.toLowerCase() && a.instanceId === b.instanceId && a.extension?.toLowerCase() === b.extension?.toLowerCase();
|
|
772
|
+
async function buildPlan(ctx, attempt, wallet) {
|
|
773
|
+
const { config, quantity } = attempt;
|
|
774
|
+
if (!Number.isInteger(quantity) || quantity < 1 || quantity > MAX_MINT_COUNT) {
|
|
775
|
+
throw new MintError("INVALID_CONFIG", `quantity must be an integer from 1 to ${MAX_MINT_COUNT}.`, { field: "quantity" });
|
|
776
|
+
}
|
|
777
|
+
const { creator, instanceId } = parseConfig(config);
|
|
778
|
+
const recipient = getAddress2(attempt.recipient);
|
|
779
|
+
const payer = getAddress2(attempt.payer);
|
|
780
|
+
let data = wallet.data;
|
|
781
|
+
if (!isWalletData(data) || !sameClaim(data.config, config) || data.recipient !== recipient) {
|
|
782
|
+
const drop = (await loadDrop(ctx, config)).drop;
|
|
783
|
+
data = (await loadWallet(ctx, config, { payer, recipient, quantity, drop })).data;
|
|
784
|
+
}
|
|
785
|
+
if (!isWalletData(data)) throw new MintError("RPC_ERROR", "Wallet read produced no plan data.");
|
|
786
|
+
const { entry, claim, fee } = data.resolved;
|
|
787
|
+
const extension = getAddress2(entry.address);
|
|
788
|
+
let mintIndices = [];
|
|
789
|
+
let merkleProofs = [];
|
|
790
|
+
if (isAllowlist(claim)) {
|
|
791
|
+
if (quantity > data.unused.length) {
|
|
792
|
+
throw new MintError("WALLET_LIMIT", `Only ${data.unused.length} allowlist spot(s) left for this wallet.`);
|
|
793
|
+
}
|
|
794
|
+
const picked = data.unused.slice(0, quantity);
|
|
795
|
+
mintIndices = picked.map((p) => p.mintIndex);
|
|
796
|
+
merkleProofs = picked.map((p) => p.proof);
|
|
797
|
+
}
|
|
798
|
+
const qty = BigInt(quantity);
|
|
799
|
+
const erc20 = claim.erc20 === zeroAddress ? null : getAddress2(claim.erc20);
|
|
800
|
+
const cost = claim.cost * qty;
|
|
801
|
+
const value = fee * qty + (erc20 ? 0n : cost);
|
|
802
|
+
let approve = null;
|
|
803
|
+
if (erc20 && cost > 0n) {
|
|
804
|
+
const allowance = await rpc(() => ctx.client.readContract({ address: erc20, abi: ERC20_ABI, functionName: "allowance", args: [payer, extension] }));
|
|
805
|
+
if (allowance < cost) {
|
|
806
|
+
approve = {
|
|
807
|
+
token: erc20,
|
|
808
|
+
spender: extension,
|
|
809
|
+
amount: cost,
|
|
810
|
+
tx: { to: erc20, data: encodeFunctionData({ abi: ERC20_ABI, functionName: "approve", args: [extension, cost] }), value: 0n }
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
approve,
|
|
816
|
+
mint: {
|
|
817
|
+
to: extension,
|
|
818
|
+
data: encodeFunctionData({
|
|
819
|
+
abi: EXTENSION_COMMON_ABI,
|
|
820
|
+
functionName: "mintProxy",
|
|
821
|
+
args: [creator, instanceId, quantity, mintIndices, merkleProofs.map((p) => [...p]), recipient]
|
|
822
|
+
}),
|
|
823
|
+
value
|
|
824
|
+
},
|
|
825
|
+
tokenContract: creator
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
var manifoldClaimAdapter = {
|
|
829
|
+
readDrop: async (ctx, config) => (await loadDrop(ctx, config)).drop,
|
|
830
|
+
readWallet: loadWallet,
|
|
831
|
+
buildPlan,
|
|
832
|
+
simulate: async (ctx, attempt, plan) => {
|
|
833
|
+
try {
|
|
834
|
+
await ctx.client.call({ account: getAddress2(attempt.payer), to: plan.mint.to, data: plan.mint.data, value: plan.mint.value });
|
|
835
|
+
} catch (e) {
|
|
836
|
+
const text2 = decodeRevert(e);
|
|
837
|
+
if (text2 === null) throw toRpcError(e);
|
|
838
|
+
throw new MintError("SIMULATION_REVERTED", text2, { cause: e });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
// src/core/adapters/contract/index.ts
|
|
844
|
+
import {
|
|
845
|
+
BaseError as BaseError2,
|
|
846
|
+
ContractFunctionRevertedError as ContractFunctionRevertedError2,
|
|
847
|
+
ContractFunctionZeroDataError,
|
|
848
|
+
InsufficientFundsError,
|
|
849
|
+
decodeErrorResult as decodeErrorResult2,
|
|
850
|
+
encodeFunctionData as encodeFunctionData2,
|
|
851
|
+
formatUnits as formatUnits3,
|
|
852
|
+
getAddress as getAddress3,
|
|
853
|
+
isAddress as isAddress2,
|
|
854
|
+
parseAbi as parseAbi2,
|
|
855
|
+
parseUnits
|
|
856
|
+
} from "viem";
|
|
857
|
+
|
|
858
|
+
// src/core/machine.ts
|
|
859
|
+
import { formatUnits as formatUnits2 } from "viem";
|
|
860
|
+
var MINT_STATES = [
|
|
861
|
+
"loading",
|
|
862
|
+
"unsupported",
|
|
863
|
+
"upcoming",
|
|
864
|
+
"ended",
|
|
865
|
+
"sold-out",
|
|
866
|
+
"disconnected",
|
|
867
|
+
"connecting",
|
|
868
|
+
"live",
|
|
869
|
+
"ineligible",
|
|
870
|
+
"insufficient-funds",
|
|
871
|
+
"switching-chain",
|
|
872
|
+
"approving",
|
|
873
|
+
"minting",
|
|
874
|
+
"confirming",
|
|
875
|
+
"success",
|
|
876
|
+
"error"
|
|
877
|
+
];
|
|
878
|
+
var CLICKABLE_STATES = ["disconnected", "live", "success", "error"];
|
|
879
|
+
var IN_FLIGHT_STATES = ["switching-chain", "approving", "minting", "confirming"];
|
|
880
|
+
var MAX_QUANTITY = 65535;
|
|
881
|
+
function initialState(opts) {
|
|
882
|
+
return {
|
|
883
|
+
name: "loading",
|
|
884
|
+
reason: null,
|
|
885
|
+
message: null,
|
|
886
|
+
readGen: 0,
|
|
887
|
+
walletGen: 0,
|
|
888
|
+
drop: null,
|
|
889
|
+
account: null,
|
|
890
|
+
walletRead: null,
|
|
891
|
+
blocked: null,
|
|
892
|
+
quantity: toQuantity(opts?.quantity ?? 1, MAX_QUANTITY),
|
|
893
|
+
attempt: null,
|
|
894
|
+
result: null,
|
|
895
|
+
pendingRefresh: false,
|
|
896
|
+
now: 0
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
function toQuantity(q, max) {
|
|
900
|
+
if (Number.isNaN(q)) return 1;
|
|
901
|
+
return Math.max(1, Math.min(Math.floor(q), max));
|
|
902
|
+
}
|
|
903
|
+
function isInFlight(s) {
|
|
904
|
+
return s.attempt !== null || IN_FLIGHT_STATES.includes(s.name);
|
|
905
|
+
}
|
|
906
|
+
function isIdle(s) {
|
|
907
|
+
return !isInFlight(s) && s.name !== "connecting" && s.name !== "success" && s.name !== "error";
|
|
908
|
+
}
|
|
909
|
+
function derive(s, unsupported) {
|
|
910
|
+
const named = (name) => ({ name, reason: null, message: null });
|
|
911
|
+
const { drop, walletRead } = s;
|
|
912
|
+
if (unsupported) return { name: "unsupported", reason: s.reason, message: s.message };
|
|
913
|
+
if (drop === null) return named("loading");
|
|
914
|
+
if (drop.endTime !== null && drop.endTime <= s.now) return named("ended");
|
|
915
|
+
if (drop.maxSupply !== null && drop.maxSupply > 0n && drop.totalMinted >= drop.maxSupply) return named("sold-out");
|
|
916
|
+
if (drop.startTime !== null && drop.startTime > s.now) return named("upcoming");
|
|
917
|
+
if (s.account === null) return named("disconnected");
|
|
918
|
+
if (walletRead === null) return named("loading");
|
|
919
|
+
if (s.blocked !== null) return s.blocked;
|
|
920
|
+
if (walletRead.ineligible !== null) {
|
|
921
|
+
return { name: "ineligible", reason: walletRead.ineligible.reason, message: walletRead.ineligible.message };
|
|
922
|
+
}
|
|
923
|
+
const total = selectTotal(s);
|
|
924
|
+
if (total !== null) {
|
|
925
|
+
const { native, erc20 } = total;
|
|
926
|
+
if (native.value + walletRead.gasReserve > walletRead.balances.native) {
|
|
927
|
+
return { name: "insufficient-funds", reason: "INSUFFICIENT_FUNDS", message: `Not enough ${native.symbol} to cover the mint.` };
|
|
928
|
+
}
|
|
929
|
+
if (erc20 !== null && walletRead.balances.erc20 !== null && erc20.value > walletRead.balances.erc20) {
|
|
930
|
+
return { name: "insufficient-funds", reason: "INSUFFICIENT_FUNDS", message: `Not enough ${erc20.symbol} to cover the mint.` };
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
return named("live");
|
|
934
|
+
}
|
|
935
|
+
function settle(s, unsupported = s.name === "unsupported") {
|
|
936
|
+
if (!isIdle(s)) return s;
|
|
937
|
+
return { ...s, ...derive(s, unsupported) };
|
|
938
|
+
}
|
|
939
|
+
function clampQuantity(s) {
|
|
940
|
+
const quantity = toQuantity(s.quantity, selectMaxQuantity(s) ?? MAX_QUANTITY);
|
|
941
|
+
return quantity === s.quantity ? s : { ...s, quantity };
|
|
942
|
+
}
|
|
943
|
+
function reducer(state, event) {
|
|
944
|
+
const inFlight = isInFlight(state);
|
|
945
|
+
const failed = (reason, message) => {
|
|
946
|
+
if (inFlight) return { ...state, pendingRefresh: true };
|
|
947
|
+
if (state.name === "success" || state.name === "error") return state;
|
|
948
|
+
return { ...state, name: "error", reason, message };
|
|
949
|
+
};
|
|
950
|
+
switch (event.type) {
|
|
951
|
+
case "LOAD": {
|
|
952
|
+
if (event.kind === "refresh") {
|
|
953
|
+
const next2 = { ...state, readGen: event.gen };
|
|
954
|
+
return inFlight ? { ...next2, pendingRefresh: true } : settle(next2);
|
|
955
|
+
}
|
|
956
|
+
const next = { ...state, readGen: event.gen, drop: null, walletRead: null, blocked: null, walletGen: state.walletGen + 1 };
|
|
957
|
+
return inFlight ? { ...next, pendingRefresh: true } : settle(next, false);
|
|
958
|
+
}
|
|
959
|
+
case "DROP_LOADED": {
|
|
960
|
+
if (event.gen !== state.readGen) return state;
|
|
961
|
+
const next = { ...state, drop: event.drop, now: Math.max(state.now, event.now) };
|
|
962
|
+
if (inFlight) return { ...next, pendingRefresh: true };
|
|
963
|
+
return isIdle(next) ? settle(clampQuantity(next), false) : clampQuantity(next);
|
|
964
|
+
}
|
|
965
|
+
case "DROP_UNSUPPORTED": {
|
|
966
|
+
if (event.gen !== state.readGen) return state;
|
|
967
|
+
if (inFlight) return { ...state, pendingRefresh: true };
|
|
968
|
+
if (state.name === "success" || state.name === "error") return state;
|
|
969
|
+
return { ...state, name: "unsupported", reason: event.reason, message: event.message };
|
|
970
|
+
}
|
|
971
|
+
case "READ_FAILED":
|
|
972
|
+
return event.gen !== state.readGen ? state : failed(event.reason, event.message);
|
|
973
|
+
case "ACCOUNT_CHANGED": {
|
|
974
|
+
const next = { ...state, account: event.account, walletRead: null, blocked: null, walletGen: state.walletGen + 1 };
|
|
975
|
+
if (inFlight) return { ...next, pendingRefresh: true };
|
|
976
|
+
if (state.name === "connecting") return event.account === null ? next : settle({ ...next, name: "loading" }, false);
|
|
977
|
+
return settle(next);
|
|
978
|
+
}
|
|
979
|
+
case "WALLET_LOADED": {
|
|
980
|
+
if (event.gen !== state.walletGen) return state;
|
|
981
|
+
const next = { ...state, walletRead: event.read, blocked: null };
|
|
982
|
+
return inFlight ? { ...next, pendingRefresh: true } : settle(clampQuantity(next));
|
|
983
|
+
}
|
|
984
|
+
case "WALLET_FAILED":
|
|
985
|
+
return event.gen !== state.walletGen ? state : failed(event.reason, event.message);
|
|
986
|
+
case "CONNECT_STARTED":
|
|
987
|
+
return state.name === "disconnected" && state.attempt === null ? { ...state, name: "connecting", reason: null, message: null } : state;
|
|
988
|
+
case "CONNECT_DISMISSED":
|
|
989
|
+
return state.name === "connecting" ? settle({ ...state, name: "disconnected" }, false) : state;
|
|
990
|
+
case "TICK":
|
|
991
|
+
return settle({ ...state, now: Math.max(state.now, event.now) });
|
|
992
|
+
case "SET_QUANTITY": {
|
|
993
|
+
const next = { ...state, quantity: toQuantity(event.quantity, selectMaxQuantity(state) ?? MAX_QUANTITY) };
|
|
994
|
+
return inFlight ? { ...next, pendingRefresh: true } : settle(next);
|
|
995
|
+
}
|
|
996
|
+
case "ATTEMPT_STARTED":
|
|
997
|
+
return state.name === "live" && state.attempt === null ? { ...state, attempt: event.attempt } : state;
|
|
998
|
+
case "SWITCHING_CHAIN":
|
|
999
|
+
return step(state, event.attemptId, "switching-chain");
|
|
1000
|
+
case "APPROVING":
|
|
1001
|
+
return step(state, event.attemptId, "approving");
|
|
1002
|
+
case "MINT_PROMPTED":
|
|
1003
|
+
return step(state, event.attemptId, "minting");
|
|
1004
|
+
case "TX_SUBMITTED":
|
|
1005
|
+
return step(state, event.attemptId, "confirming");
|
|
1006
|
+
case "ATTEMPT_SUCCEEDED":
|
|
1007
|
+
return state.attempt?.attemptId === event.attemptId ? {
|
|
1008
|
+
...state,
|
|
1009
|
+
name: "success",
|
|
1010
|
+
reason: null,
|
|
1011
|
+
message: null,
|
|
1012
|
+
attempt: null,
|
|
1013
|
+
result: event.result,
|
|
1014
|
+
drop: null,
|
|
1015
|
+
readGen: state.readGen + 1
|
|
1016
|
+
} : state;
|
|
1017
|
+
case "ATTEMPT_FAILED": {
|
|
1018
|
+
if (state.attempt?.attemptId !== event.attemptId) return state;
|
|
1019
|
+
const { reason, message } = event;
|
|
1020
|
+
const done = { ...state, reason, message, attempt: null, result: null };
|
|
1021
|
+
if (INELIGIBLE_REASONS.includes(reason)) {
|
|
1022
|
+
return { ...done, name: "ineligible", blocked: { name: "ineligible", reason, message } };
|
|
1023
|
+
}
|
|
1024
|
+
if (reason === "INSUFFICIENT_FUNDS") {
|
|
1025
|
+
return { ...done, name: "insufficient-funds", blocked: { name: "insufficient-funds", reason, message } };
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
...done,
|
|
1029
|
+
name: "error",
|
|
1030
|
+
drop: state.name === "confirming" ? null : state.drop,
|
|
1031
|
+
readGen: state.name === "confirming" ? state.readGen + 1 : state.readGen
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
case "RESET": {
|
|
1035
|
+
if (state.name !== "success" && state.name !== "error") return state;
|
|
1036
|
+
const next = {
|
|
1037
|
+
...state,
|
|
1038
|
+
readGen: event.gen,
|
|
1039
|
+
walletRead: null,
|
|
1040
|
+
blocked: null,
|
|
1041
|
+
walletGen: state.walletGen + 1,
|
|
1042
|
+
result: null,
|
|
1043
|
+
pendingRefresh: false
|
|
1044
|
+
};
|
|
1045
|
+
return settle(clampQuantity({ ...next, name: "loading" }), false);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
var STEP_ORDER = ["switching-chain", "approving", "minting", "confirming"];
|
|
1050
|
+
function step(state, attemptId, name) {
|
|
1051
|
+
if (state.attempt?.attemptId !== attemptId) return state;
|
|
1052
|
+
if (STEP_ORDER.indexOf(name) <= STEP_ORDER.indexOf(state.name)) return state;
|
|
1053
|
+
return { ...state, name, reason: null, message: null };
|
|
1054
|
+
}
|
|
1055
|
+
function selectMaxQuantity(state) {
|
|
1056
|
+
const { drop, walletRead } = state;
|
|
1057
|
+
if (drop === null) return null;
|
|
1058
|
+
let max = MAX_QUANTITY;
|
|
1059
|
+
if (drop.maxSupply !== null && drop.maxSupply > 0n) {
|
|
1060
|
+
const remaining = drop.maxSupply - drop.totalMinted;
|
|
1061
|
+
if (remaining < BigInt(max)) max = remaining > 0n ? Number(remaining) : 0;
|
|
1062
|
+
}
|
|
1063
|
+
if (walletRead !== null && walletRead.maxQuantity !== null) {
|
|
1064
|
+
max = Math.min(max, Math.max(0, Math.floor(walletRead.maxQuantity)));
|
|
1065
|
+
}
|
|
1066
|
+
return max;
|
|
1067
|
+
}
|
|
1068
|
+
function selectTotal(state) {
|
|
1069
|
+
const { drop } = state;
|
|
1070
|
+
if (drop === null) return null;
|
|
1071
|
+
const q = BigInt(state.quantity);
|
|
1072
|
+
const price = drop.unitPrice;
|
|
1073
|
+
const fee = drop.unitFee;
|
|
1074
|
+
const priceTotal = drop.pricePer === "total" ? price.value : price.value * q;
|
|
1075
|
+
if (drop.paymentToken !== null) {
|
|
1076
|
+
return {
|
|
1077
|
+
native: money2(fee.value * q, fee),
|
|
1078
|
+
erc20: money2(priceTotal, price)
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
return { native: money2(priceTotal + fee.value * q, price), erc20: null };
|
|
1082
|
+
}
|
|
1083
|
+
function money2(value, like) {
|
|
1084
|
+
return { value, decimals: like.decimals, symbol: like.symbol, formatted: formatUnits2(value, like.decimals) };
|
|
1085
|
+
}
|
|
1086
|
+
function canAct(state) {
|
|
1087
|
+
return state.attempt === null && CLICKABLE_STATES.includes(state.name);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/core/adapters/contract/index.ts
|
|
1091
|
+
var PLACEHOLDERS = ["{recipient}", "{payer}", "{quantity}", "{unitPrice}", "{totalPrice}"];
|
|
1092
|
+
var ERC20_ABI2 = parseAbi2([
|
|
1093
|
+
"function symbol() view returns (string)",
|
|
1094
|
+
"function decimals() view returns (uint8)",
|
|
1095
|
+
"function balanceOf(address) view returns (uint256)",
|
|
1096
|
+
"function allowance(address,address) view returns (uint256)",
|
|
1097
|
+
"function approve(address,uint256) returns (bool)"
|
|
1098
|
+
]);
|
|
1099
|
+
var FALLBACK_MINT_GAS = 300000n;
|
|
1100
|
+
var APPROVE_GAS = 65000n;
|
|
1101
|
+
var PLACEHOLDER_TOKEN = /\{[A-Za-z_]\w*\}/g;
|
|
1102
|
+
var DISPLAY_AMOUNT = /^\d+(\.\d+)?$/;
|
|
1103
|
+
var NUMERIC_TYPE = /^u?int\d*$/;
|
|
1104
|
+
function bad(field, message) {
|
|
1105
|
+
throw new MintError("INVALID_CONFIG", `${field} ${message}`, { field });
|
|
1106
|
+
}
|
|
1107
|
+
var isHexAddress = (a) => typeof a === "string" && isAddress2(a, { strict: false });
|
|
1108
|
+
function functionsNamed(abi, name) {
|
|
1109
|
+
return abi.filter((i) => i.type === "function" && i.name === name);
|
|
1110
|
+
}
|
|
1111
|
+
function validateTemplate(t, path, allowed) {
|
|
1112
|
+
if (typeof t === "string") {
|
|
1113
|
+
for (const token of t.match(PLACEHOLDER_TOKEN) ?? []) {
|
|
1114
|
+
if (!PLACEHOLDERS.includes(token)) bad(path, `"${t}" is not a known placeholder (${token})`);
|
|
1115
|
+
if (!allowed.includes(token)) bad(path, `"${t}" uses ${token}, which is not available in this field`);
|
|
1116
|
+
}
|
|
1117
|
+
} else if (Array.isArray(t)) {
|
|
1118
|
+
t.forEach((x, i) => validateTemplate(x, `${path}[${i}]`, allowed));
|
|
1119
|
+
} else if (typeof t === "number") {
|
|
1120
|
+
if (!Number.isSafeInteger(t)) bad(path, `${t} is not a safe integer; pass larger or fractional values as a bigint or decimal string`);
|
|
1121
|
+
} else if (typeof t !== "bigint" && typeof t !== "boolean") {
|
|
1122
|
+
bad(path, "must be a string, number, bigint, boolean or array");
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
var ARRAY_TYPE = /^(.*)\[(\d*)\]$/;
|
|
1126
|
+
var BYTES_TYPE = /^bytes(\d*)$/;
|
|
1127
|
+
var INT_TYPE = /^(u?)int(\d*)$/;
|
|
1128
|
+
var isExactPlaceholder = (t) => typeof t === "string" && PLACEHOLDERS.includes(t);
|
|
1129
|
+
var hasPlaceholder = (t) => typeof t === "string" && (t.match(PLACEHOLDER_TOKEN) ?? []).length > 0;
|
|
1130
|
+
function intBounds(type) {
|
|
1131
|
+
const m = INT_TYPE.exec(type);
|
|
1132
|
+
if (!m) return null;
|
|
1133
|
+
const bits = BigInt(m[2] ? Number(m[2]) : 256);
|
|
1134
|
+
return m[1] ? { min: 0n, max: 2n ** bits - 1n } : { min: -(2n ** (bits - 1n)), max: 2n ** (bits - 1n) - 1n };
|
|
1135
|
+
}
|
|
1136
|
+
var elementParam = (p, type) => ({ ...p, type });
|
|
1137
|
+
var componentsOf = (p) => p.components ?? [];
|
|
1138
|
+
function typeIssue(t, p, path) {
|
|
1139
|
+
const no = (message) => ({ path, message });
|
|
1140
|
+
const arr = ARRAY_TYPE.exec(p.type);
|
|
1141
|
+
if (arr) {
|
|
1142
|
+
if (!Array.isArray(t)) return no(`must be an array for ${p.type}`);
|
|
1143
|
+
if (arr[2] !== "" && t.length !== Number(arr[2])) return no(`has ${t.length} item(s) but ${p.type} takes ${arr[2]}`);
|
|
1144
|
+
const el = elementParam(p, arr[1]);
|
|
1145
|
+
for (let i = 0; i < t.length; i++) {
|
|
1146
|
+
const issue = typeIssue(t[i], el, `${path}[${i}]`);
|
|
1147
|
+
if (issue) return issue;
|
|
1148
|
+
}
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
if (p.type === "tuple") {
|
|
1152
|
+
const comps = componentsOf(p);
|
|
1153
|
+
if (!Array.isArray(t)) return no("must be an array for a tuple");
|
|
1154
|
+
if (t.length !== comps.length) return no(`has ${t.length} item(s) but the tuple takes ${comps.length}`);
|
|
1155
|
+
for (let i = 0; i < comps.length; i++) {
|
|
1156
|
+
const issue = typeIssue(t[i], comps[i], `${path}[${i}]`);
|
|
1157
|
+
if (issue) return issue;
|
|
1158
|
+
}
|
|
1159
|
+
return null;
|
|
1160
|
+
}
|
|
1161
|
+
if (Array.isArray(t)) return no(`must not be an array for ${p.type}`);
|
|
1162
|
+
if (p.type === "address") {
|
|
1163
|
+
if (isExactPlaceholder(t)) return t === "{recipient}" || t === "{payer}" ? null : no(`${t} is a number, but the abi input is an address`);
|
|
1164
|
+
return typeof t === "string" && !hasPlaceholder(t) && isAddress2(t) ? null : no(`${JSON.stringify(String(t))} is not an address`);
|
|
1165
|
+
}
|
|
1166
|
+
if (p.type === "bool") return typeof t === "boolean" ? null : no(`${JSON.stringify(String(t))} is not a boolean`);
|
|
1167
|
+
if (p.type === "string") return typeof t === "string" ? null : no(`${JSON.stringify(String(t))} is not a string`);
|
|
1168
|
+
const bytes = BYTES_TYPE.exec(p.type);
|
|
1169
|
+
if (bytes) {
|
|
1170
|
+
const ok = typeof t === "string" && !hasPlaceholder(t) && /^0x([0-9a-fA-F]{2})*$/.test(t) && (!bytes[1] || t.length === 2 + 2 * Number(bytes[1]));
|
|
1171
|
+
return ok ? null : no(`${JSON.stringify(String(t))} is not ${bytes[1] ? `${bytes[1]} bytes of` : ""} hex data`);
|
|
1172
|
+
}
|
|
1173
|
+
const bounds = intBounds(p.type);
|
|
1174
|
+
if (bounds) {
|
|
1175
|
+
let n;
|
|
1176
|
+
if (isExactPlaceholder(t)) return t === "{recipient}" || t === "{payer}" ? no(`${t} is an address, but the abi input is ${p.type}`) : null;
|
|
1177
|
+
if (typeof t === "bigint") n = t;
|
|
1178
|
+
else if (typeof t === "number") n = BigInt(t);
|
|
1179
|
+
else if (typeof t === "string" && !hasPlaceholder(t) && /^-?\d+$/.test(t)) n = BigInt(t);
|
|
1180
|
+
else return no(`${JSON.stringify(String(t))} is not an integer for ${p.type}`);
|
|
1181
|
+
return n >= bounds.min && n <= bounds.max ? null : no(`${n} is out of range for ${p.type}`);
|
|
1182
|
+
}
|
|
1183
|
+
return no(`abi type ${p.type} is not supported in an argument template`);
|
|
1184
|
+
}
|
|
1185
|
+
var signature = (f) => `${f.name}(${f.inputs.map((i) => i.type).join(",")})`;
|
|
1186
|
+
var NO_PLACEHOLDERS = [];
|
|
1187
|
+
function validateCall(abi, call, prefix, kind, allowed) {
|
|
1188
|
+
const c = call ?? {};
|
|
1189
|
+
const named = typeof c.functionName === "string" ? functionsNamed(abi, c.functionName) : [];
|
|
1190
|
+
if (named.length === 0) bad(`${prefix}.functionName`, `"${String(c.functionName)}" is not a function in the abi`);
|
|
1191
|
+
if (c.args !== void 0 && !Array.isArray(c.args)) bad(`${prefix}.args`, "must be an array");
|
|
1192
|
+
if (kind === "mint" && c.args === void 0) bad(`${prefix}.args`, "is required");
|
|
1193
|
+
const args = c.args ?? [];
|
|
1194
|
+
args.forEach((a, i) => validateTemplate(a, `${prefix}.args[${i}]`, allowed));
|
|
1195
|
+
const sameArity = named.filter((f) => f.inputs.length === args.length);
|
|
1196
|
+
if (sameArity.length === 0) {
|
|
1197
|
+
const want = [...new Set(named.map((f) => f.inputs.length))].join(" or ");
|
|
1198
|
+
bad(`${prefix}.args`, `has ${args.length} argument(s) but "${String(c.functionName)}" takes ${want}`);
|
|
1199
|
+
}
|
|
1200
|
+
const checked = sameArity.map((f) => {
|
|
1201
|
+
let issue = null;
|
|
1202
|
+
for (let i = 0; i < args.length && !issue; i++) issue = typeIssue(args[i], f.inputs[i], `${prefix}.args[${i}]`);
|
|
1203
|
+
return { f, issue };
|
|
1204
|
+
});
|
|
1205
|
+
const fits = checked.filter((x) => !x.issue);
|
|
1206
|
+
if (fits.length === 0) {
|
|
1207
|
+
const first = checked[0].issue;
|
|
1208
|
+
if (sameArity.length === 1) bad(first.path, first.message);
|
|
1209
|
+
bad(`${prefix}.args`, `fit none of the overloads ${sameArity.map(signature).join(", ")}: ${first.path} ${first.message}`);
|
|
1210
|
+
}
|
|
1211
|
+
const roleFits = fits.filter(
|
|
1212
|
+
({ f }) => kind === "read" ? f.stateMutability === "view" || f.stateMutability === "pure" : f.stateMutability === "payable" || f.stateMutability === "nonpayable"
|
|
1213
|
+
);
|
|
1214
|
+
if (roleFits.length === 0) {
|
|
1215
|
+
const name = fits[0].f.name;
|
|
1216
|
+
if (kind === "read") bad(`${prefix}.functionName`, `"${name}" is not a view or pure function`);
|
|
1217
|
+
if (fits.some(({ f }) => f.stateMutability !== "view" && f.stateMutability !== "pure")) {
|
|
1218
|
+
bad(`${prefix}.functionName`, `"${name}" has missing or invalid stateMutability; mint requires payable or nonpayable`);
|
|
1219
|
+
}
|
|
1220
|
+
bad(`${prefix}.functionName`, `"${name}" is a view or pure function and cannot mint`);
|
|
1221
|
+
}
|
|
1222
|
+
if (roleFits.length > 1) {
|
|
1223
|
+
bad(`${prefix}.functionName`, `"${String(c.functionName)}" is ambiguous: ${roleFits.map((x) => signature(x.f)).join(" and ")} both accept these arguments`);
|
|
1224
|
+
}
|
|
1225
|
+
return roleFits[0].f;
|
|
1226
|
+
}
|
|
1227
|
+
function resolveOutput(fn, output, path) {
|
|
1228
|
+
const outs = fn.outputs;
|
|
1229
|
+
if (outs.length === 0) bad(`${path}.functionName`, `"${fn.name}" returns nothing`);
|
|
1230
|
+
const first = outs[0];
|
|
1231
|
+
const tuple = outs.length === 1 && first.type === "tuple";
|
|
1232
|
+
const members = tuple ? first.components ?? [] : outs;
|
|
1233
|
+
const memberNames = members.map((m, i) => m.name || String(i)).join(", ");
|
|
1234
|
+
let index;
|
|
1235
|
+
if (output === void 0) {
|
|
1236
|
+
if (members.length !== 1) bad(`${path}.output`, `is required: "${fn.name}" returns several values (${memberNames})`);
|
|
1237
|
+
index = 0;
|
|
1238
|
+
} else if (typeof output === "number") {
|
|
1239
|
+
if (!Number.isInteger(output) || output < 0 || output >= members.length) {
|
|
1240
|
+
bad(`${path}.output`, `${output} is not in the abi outputs of "${fn.name}" (${memberNames})`);
|
|
1241
|
+
}
|
|
1242
|
+
index = output;
|
|
1243
|
+
} else if (typeof output === "string") {
|
|
1244
|
+
index = members.findIndex((m) => m.name === output);
|
|
1245
|
+
if (index < 0) bad(`${path}.output`, `"${output}" is not in the abi outputs of "${fn.name}" (${memberNames})`);
|
|
1246
|
+
} else {
|
|
1247
|
+
return bad(`${path}.output`, "must be an index or a name");
|
|
1248
|
+
}
|
|
1249
|
+
const type = members[index].type;
|
|
1250
|
+
if (!NUMERIC_TYPE.test(type)) {
|
|
1251
|
+
const reason = `returns ${type}, not a number`;
|
|
1252
|
+
if (output === void 0) bad(`${path}.functionName`, `"${fn.name}" ${reason}`);
|
|
1253
|
+
bad(`${path}.output`, `selects a value that is ${type}, not a number`);
|
|
1254
|
+
}
|
|
1255
|
+
return { tuple, index, type };
|
|
1256
|
+
}
|
|
1257
|
+
function validateField(abi, name, src, allowed, isPrice) {
|
|
1258
|
+
if (typeof src !== "object" || src === null) bad(name, "is required and must be { value } or { read }");
|
|
1259
|
+
const s = src;
|
|
1260
|
+
const hasRead = Object.hasOwn(s, "read");
|
|
1261
|
+
const hasValue = Object.hasOwn(s, "value");
|
|
1262
|
+
if (hasRead && hasValue) bad(name, "must be exactly one of { value } or { read }, not both");
|
|
1263
|
+
if (hasRead) {
|
|
1264
|
+
if (typeof s.read !== "object" || s.read === null) bad(`${name}.read`, "must be { functionName, args?, output? }");
|
|
1265
|
+
if (s.unit !== void 0) bad(name, "unit is only valid with a string value");
|
|
1266
|
+
const fn = validateCall(abi, s.read, `${name}.read`, "read", allowed);
|
|
1267
|
+
resolveOutput(fn, s.read.output, `${name}.read`);
|
|
1268
|
+
} else if (typeof s.value === "bigint") {
|
|
1269
|
+
if (s.unit !== void 0) bad(name, "unit is only valid with a string value");
|
|
1270
|
+
if (s.value < 0n) bad(`${name}.value`, "must not be negative");
|
|
1271
|
+
} else if (typeof s.value === "string") {
|
|
1272
|
+
if (s.unit !== "display") bad(name, 'string values need unit: "display"');
|
|
1273
|
+
if (!isPrice) bad(name, "display units are only valid for price; use a bigint in base units");
|
|
1274
|
+
if (!DISPLAY_AMOUNT.test(s.value)) bad(`${name}.value`, `"${s.value}" is not a decimal number like "0.01"`);
|
|
1275
|
+
} else {
|
|
1276
|
+
bad(name, 'must be { value: bigint }, { value: string, unit: "display" } (price only) or { read: {...} }');
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function hasNonzeroStaticPrice(price) {
|
|
1280
|
+
if (typeof price !== "object" || price === null || !Object.hasOwn(price, "value")) return false;
|
|
1281
|
+
const v = price.value;
|
|
1282
|
+
return typeof v === "bigint" ? v > 0n : typeof v === "string" && /[1-9]/.test(v);
|
|
1283
|
+
}
|
|
1284
|
+
function validateContractConfig(config) {
|
|
1285
|
+
const c = config;
|
|
1286
|
+
if (!c || typeof c !== "object") bad("config", "must be an object");
|
|
1287
|
+
if (c.type !== "contract") bad("type", 'must be "contract"');
|
|
1288
|
+
if (!Number.isSafeInteger(c.chainId) || c.chainId <= 0) bad("chainId", "must be a positive safe integer");
|
|
1289
|
+
if (!isHexAddress(c.address)) bad("address", `"${String(c.address)}" is not a valid address`);
|
|
1290
|
+
if (c.tokenContract !== void 0 && !isHexAddress(c.tokenContract)) {
|
|
1291
|
+
bad("tokenContract", `"${String(c.tokenContract)}" is not a valid address`);
|
|
1292
|
+
}
|
|
1293
|
+
if (!Array.isArray(c.abi)) bad("abi", "must be an abi array");
|
|
1294
|
+
const abi = c.abi;
|
|
1295
|
+
const mintFn = validateCall(abi, c.mint, "mint", "mint", PLACEHOLDERS);
|
|
1296
|
+
if (c.price === void 0 || c.price === null) bad("price", "is required");
|
|
1297
|
+
validateField(abi, "price", c.price, NO_PLACEHOLDERS, true);
|
|
1298
|
+
const cur = c.price.currency;
|
|
1299
|
+
if (cur !== "native" && !(typeof cur === "object" && cur !== null && isHexAddress(cur.address))) {
|
|
1300
|
+
bad("price.currency", 'must be "native" or { address } with a valid ERC20 address');
|
|
1301
|
+
}
|
|
1302
|
+
if (cur === "native" && mintFn.stateMutability === "nonpayable" && hasNonzeroStaticPrice(c.price)) {
|
|
1303
|
+
bad("price", `is a native price, but "${mintFn.name}" is nonpayable and cannot take value`);
|
|
1304
|
+
}
|
|
1305
|
+
const per = c.price.per;
|
|
1306
|
+
if (per !== void 0 && per !== "unit" && per !== "total") bad("price.per", 'must be "unit" or "total"');
|
|
1307
|
+
validateOptional(abi, "totalMinted", c.totalMinted, NO_PLACEHOLDERS);
|
|
1308
|
+
validateOptional(abi, "maxSupply", c.maxSupply, NO_PLACEHOLDERS);
|
|
1309
|
+
validateOptional(abi, "startTime", c.startTime, NO_PLACEHOLDERS);
|
|
1310
|
+
validateOptional(abi, "endTime", c.endTime, NO_PLACEHOLDERS);
|
|
1311
|
+
validateOptional(abi, "walletMinted", c.walletMinted, PLACEHOLDERS);
|
|
1312
|
+
validateOptional(abi, "walletMax", c.walletMax, PLACEHOLDERS);
|
|
1313
|
+
}
|
|
1314
|
+
function validateOptional(abi, name, src, allowed) {
|
|
1315
|
+
if (src !== void 0) validateField(abi, name, src, allowed, false);
|
|
1316
|
+
}
|
|
1317
|
+
function varFor(token, vars) {
|
|
1318
|
+
const key = token.slice(1, -1);
|
|
1319
|
+
const v = vars[key];
|
|
1320
|
+
if (v === void 0) throw new MintError("INVALID_CONFIG", `placeholder ${token} has no value here`);
|
|
1321
|
+
return typeof v === "string" ? getAddress3(v) : v;
|
|
1322
|
+
}
|
|
1323
|
+
function fill(t, p, vars) {
|
|
1324
|
+
const arr = ARRAY_TYPE.exec(p.type);
|
|
1325
|
+
if (arr && Array.isArray(t)) return t.map((x) => fill(x, elementParam(p, arr[1]), vars));
|
|
1326
|
+
if (p.type === "tuple" && Array.isArray(t)) return t.map((x, i) => fill(x, componentsOf(p)[i], vars));
|
|
1327
|
+
if (typeof t === "number") return BigInt(t);
|
|
1328
|
+
if (typeof t !== "string") return t;
|
|
1329
|
+
if (p.type === "string") return t.replace(PLACEHOLDER_TOKEN, (token) => String(varFor(token, vars)));
|
|
1330
|
+
if (isExactPlaceholder(t)) return varFor(t, vars);
|
|
1331
|
+
if (intBounds(p.type)) return BigInt(t);
|
|
1332
|
+
return t;
|
|
1333
|
+
}
|
|
1334
|
+
var fillArgs = (args, inputs, vars) => (args ?? []).map((a, i) => fill(a, inputs[i], vars));
|
|
1335
|
+
var totalFor = (per, price, quantity) => per === "total" ? price : price * quantity;
|
|
1336
|
+
function shortMessage(e) {
|
|
1337
|
+
return e instanceof BaseError2 ? e.shortMessage : e instanceof Error ? e.message : String(e);
|
|
1338
|
+
}
|
|
1339
|
+
var abortError = () => new DOMException("The operation was aborted", "AbortError");
|
|
1340
|
+
var isAbortError = (e) => e?.name === "AbortError";
|
|
1341
|
+
function throwIfAborted(ctx) {
|
|
1342
|
+
if (ctx.signal?.aborted) throw abortError();
|
|
1343
|
+
}
|
|
1344
|
+
function io(ctx, start) {
|
|
1345
|
+
const signal = ctx.signal;
|
|
1346
|
+
if (!signal) return start();
|
|
1347
|
+
if (signal.aborted) return Promise.reject(abortError());
|
|
1348
|
+
return new Promise((resolve, reject) => {
|
|
1349
|
+
const onAbort = () => reject(abortError());
|
|
1350
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1351
|
+
start().then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
function rpcError(e) {
|
|
1355
|
+
if (isMintError(e) || isAbortError(e)) throw e;
|
|
1356
|
+
throw new MintError("RPC_ERROR", `RPC request failed: ${shortMessage(e)}`, { cause: e });
|
|
1357
|
+
}
|
|
1358
|
+
function classifyReadError(e, field) {
|
|
1359
|
+
if (isMintError(e) || isAbortError(e)) throw e;
|
|
1360
|
+
if (e instanceof BaseError2 && e.walk((x) => x instanceof ContractFunctionRevertedError2 || x instanceof ContractFunctionZeroDataError)) {
|
|
1361
|
+
throw new MintError("INVALID_CONFIG", `${field} read reverted or returned no data: ${e.shortMessage}`, { field, cause: e });
|
|
1362
|
+
}
|
|
1363
|
+
return rpcError(e);
|
|
1364
|
+
}
|
|
1365
|
+
async function readCall(ctx, params, field) {
|
|
1366
|
+
try {
|
|
1367
|
+
return await io(ctx, () => ctx.client.readContract(params));
|
|
1368
|
+
} catch (e) {
|
|
1369
|
+
return classifyReadError(e, field);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function toBigInt(v, field) {
|
|
1373
|
+
const n = typeof v === "number" ? BigInt(v) : v;
|
|
1374
|
+
if (typeof n !== "bigint") throw new MintError("INVALID_CONFIG", `${field} did not return a number`, { field });
|
|
1375
|
+
if (n < 0n) throw new MintError("INVALID_CONFIG", `${field} returned a negative number`, { field });
|
|
1376
|
+
return n;
|
|
1377
|
+
}
|
|
1378
|
+
async function readField(ctx, config, name, src, vars) {
|
|
1379
|
+
if (Object.hasOwn(src, "read") && src.read != null) {
|
|
1380
|
+
const read = src.read;
|
|
1381
|
+
const path = `${name}.read`;
|
|
1382
|
+
const fn = validateCall(config.abi, read, path, "read", PLACEHOLDERS);
|
|
1383
|
+
const args = fillArgs(read.args, fn.inputs, vars);
|
|
1384
|
+
const ref = resolveOutput(fn, read.output, path);
|
|
1385
|
+
const res = await readCall(ctx, { address: config.address, abi: [fn], functionName: fn.name, args }, `${path}.functionName`);
|
|
1386
|
+
const members = fn.outputs.length === 1 && !ref.tuple ? [res] : ref.tuple ? tupleMembers(res, fn, ref.index) : res;
|
|
1387
|
+
return toBigInt(members[ref.index], path);
|
|
1388
|
+
}
|
|
1389
|
+
const value = src.value;
|
|
1390
|
+
if (typeof value === "bigint") return value;
|
|
1391
|
+
throw new MintError("INVALID_CONFIG", `${name} display units are only valid for price`, { field: name });
|
|
1392
|
+
}
|
|
1393
|
+
function tupleMembers(res, fn, index) {
|
|
1394
|
+
if (Array.isArray(res)) return res;
|
|
1395
|
+
const comps = fn.outputs[0].components ?? [];
|
|
1396
|
+
const out = [];
|
|
1397
|
+
out[index] = res[comps[index]?.name ?? ""];
|
|
1398
|
+
return out;
|
|
1399
|
+
}
|
|
1400
|
+
var optField = (ctx, config, name, src, vars) => src ? readField(ctx, config, name, src, vars) : Promise.resolve(null);
|
|
1401
|
+
function toSeconds(v, field) {
|
|
1402
|
+
if (v === null || v === 0n) return null;
|
|
1403
|
+
if (v > BigInt(Number.MAX_SAFE_INTEGER)) throw new MintError("INVALID_CONFIG", `${field} is too large for a unix timestamp`, { field });
|
|
1404
|
+
return Number(v);
|
|
1405
|
+
}
|
|
1406
|
+
function nativeMeta(ctx) {
|
|
1407
|
+
const n = ctx.client.chain?.nativeCurrency;
|
|
1408
|
+
return { decimals: n?.decimals ?? 18, symbol: n?.symbol ?? "ETH" };
|
|
1409
|
+
}
|
|
1410
|
+
var erc20Of = (config) => config.price.currency === "native" ? null : getAddress3(config.price.currency.address);
|
|
1411
|
+
async function currencyMeta(ctx, config) {
|
|
1412
|
+
const token = erc20Of(config);
|
|
1413
|
+
if (!token) return { token, ...nativeMeta(ctx) };
|
|
1414
|
+
const field = "price.currency.address";
|
|
1415
|
+
const [symbol, decimals] = await Promise.all([
|
|
1416
|
+
readCall(ctx, { address: token, abi: ERC20_ABI2, functionName: "symbol" }, field),
|
|
1417
|
+
readCall(ctx, { address: token, abi: ERC20_ABI2, functionName: "decimals" }, field)
|
|
1418
|
+
]);
|
|
1419
|
+
if (typeof symbol !== "string" || typeof decimals !== "number") {
|
|
1420
|
+
throw new MintError("INVALID_CONFIG", `${field} ${token} is not an ERC20 (symbol/decimals unreadable)`, { field });
|
|
1421
|
+
}
|
|
1422
|
+
return { token, decimals, symbol };
|
|
1423
|
+
}
|
|
1424
|
+
function parseDisplay(value, decimals) {
|
|
1425
|
+
const frac = value.split(".")[1] ?? "";
|
|
1426
|
+
if (frac.length > decimals) {
|
|
1427
|
+
throw new MintError("INVALID_CONFIG", `price.value "${value}" has more than ${decimals} decimal places`, { field: "price.value" });
|
|
1428
|
+
}
|
|
1429
|
+
return parseUnits(value, decimals);
|
|
1430
|
+
}
|
|
1431
|
+
async function resolvePrice(ctx, config, meta) {
|
|
1432
|
+
const price = config.price;
|
|
1433
|
+
if (Object.hasOwn(price, "read")) return readField(ctx, config, "price", price, {});
|
|
1434
|
+
const value = price.value;
|
|
1435
|
+
if (typeof value === "bigint") return value;
|
|
1436
|
+
return parseDisplay(value, (meta ?? await currencyMeta(ctx, config)).decimals);
|
|
1437
|
+
}
|
|
1438
|
+
var money3 = (value, decimals, symbol) => ({
|
|
1439
|
+
value,
|
|
1440
|
+
decimals,
|
|
1441
|
+
symbol,
|
|
1442
|
+
formatted: formatUnits3(value, decimals)
|
|
1443
|
+
});
|
|
1444
|
+
function revertData2(e) {
|
|
1445
|
+
for (let cur = e, depth = 0; cur && depth < 12; depth++) {
|
|
1446
|
+
const d = cur.data;
|
|
1447
|
+
if (typeof d === "string" && d.startsWith("0x") && d.length >= 10) return d;
|
|
1448
|
+
if (d && typeof d === "object" && typeof d.data === "string") {
|
|
1449
|
+
const inner = d.data;
|
|
1450
|
+
if (inner.startsWith("0x") && inner.length >= 10) return inner;
|
|
1451
|
+
}
|
|
1452
|
+
cur = cur.cause;
|
|
1453
|
+
}
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
function describeRevert(abi, data) {
|
|
1457
|
+
try {
|
|
1458
|
+
const d = decodeErrorResult2({ abi, data });
|
|
1459
|
+
const args = d.args ?? [];
|
|
1460
|
+
if (d.errorName === "Error") return String(args[0] ?? "execution reverted");
|
|
1461
|
+
const inputs = d.abiItem?.inputs ?? [];
|
|
1462
|
+
const shown = args.map((a, i) => inputs[i]?.name ? `${inputs[i].name}: ${String(a)}` : String(a));
|
|
1463
|
+
return `${d.errorName}(${shown.join(", ")})`;
|
|
1464
|
+
} catch {
|
|
1465
|
+
return `reverted with unrecognized error ${data.slice(0, 10)}`;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function toSimulationError(e, abi) {
|
|
1469
|
+
if (isMintError(e)) return e;
|
|
1470
|
+
if (isAbortError(e)) throw e;
|
|
1471
|
+
if (e instanceof BaseError2 && e.walk((x) => x instanceof InsufficientFundsError)) {
|
|
1472
|
+
return new MintError("INSUFFICIENT_FUNDS", `Payer cannot cover the mint: ${e.shortMessage}`, { cause: e });
|
|
1473
|
+
}
|
|
1474
|
+
const data = revertData2(e);
|
|
1475
|
+
if (data) return new MintError("SIMULATION_REVERTED", describeRevert(abi, data), { cause: e });
|
|
1476
|
+
const isRevert = e instanceof BaseError2 && e.walk((x) => x.code === 3 || /execution reverted/i.test(x.message));
|
|
1477
|
+
if (isRevert) return new MintError("SIMULATION_REVERTED", shortMessage(e), { cause: e });
|
|
1478
|
+
return new MintError("RPC_ERROR", `RPC request failed: ${shortMessage(e)}`, { cause: e });
|
|
1479
|
+
}
|
|
1480
|
+
async function simulateMint(ctx, config, call) {
|
|
1481
|
+
try {
|
|
1482
|
+
await io(ctx, () => ctx.client.call({ account: call.payer, to: call.to, data: call.data, value: call.value }));
|
|
1483
|
+
} catch (e) {
|
|
1484
|
+
throw toSimulationError(e, config.abi);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function encodeMint(config, vars) {
|
|
1488
|
+
const fn = validateCall(config.abi, config.mint, "mint", "mint", PLACEHOLDERS);
|
|
1489
|
+
try {
|
|
1490
|
+
return encodeFunctionData2({ abi: [fn], functionName: fn.name, args: fillArgs(config.mint.args, fn.inputs, vars) });
|
|
1491
|
+
} catch (e) {
|
|
1492
|
+
if (isMintError(e)) throw e;
|
|
1493
|
+
throw new MintError("INVALID_CONFIG", `mint.args cannot be encoded for ${signature(fn)}: ${shortMessage(e)}`, { field: "mint.args", cause: e });
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
function mintValue(config, token, totalPrice) {
|
|
1497
|
+
if (token) return 0n;
|
|
1498
|
+
const fn = validateCall(config.abi, config.mint, "mint", "mint", PLACEHOLDERS);
|
|
1499
|
+
if (fn.stateMutability === "nonpayable") {
|
|
1500
|
+
if (totalPrice > 0n) bad("price", `is ${totalPrice} wei in native currency, but "${fn.name}" is nonpayable and cannot take value`);
|
|
1501
|
+
return 0n;
|
|
1502
|
+
}
|
|
1503
|
+
return totalPrice;
|
|
1504
|
+
}
|
|
1505
|
+
async function readDrop(ctx, config) {
|
|
1506
|
+
validateContractConfig(config);
|
|
1507
|
+
throwIfAborted(ctx);
|
|
1508
|
+
const meta = await currencyMeta(ctx, config);
|
|
1509
|
+
const none = {};
|
|
1510
|
+
const [price, totalMinted, maxSupply, start, end] = await Promise.all([
|
|
1511
|
+
resolvePrice(ctx, config, meta),
|
|
1512
|
+
optField(ctx, config, "totalMinted", config.totalMinted, none),
|
|
1513
|
+
optField(ctx, config, "maxSupply", config.maxSupply, none),
|
|
1514
|
+
optField(ctx, config, "startTime", config.startTime, none),
|
|
1515
|
+
optField(ctx, config, "endTime", config.endTime, none)
|
|
1516
|
+
]);
|
|
1517
|
+
mintValue(config, meta.token, price);
|
|
1518
|
+
const native = nativeMeta(ctx);
|
|
1519
|
+
return {
|
|
1520
|
+
chainId: config.chainId,
|
|
1521
|
+
unitPrice: money3(price, meta.decimals, meta.symbol),
|
|
1522
|
+
unitFee: money3(0n, native.decimals, native.symbol),
|
|
1523
|
+
totalMinted: totalMinted ?? 0n,
|
|
1524
|
+
maxSupply: maxSupply === null || maxSupply === 0n ? null : maxSupply,
|
|
1525
|
+
startTime: toSeconds(start, "startTime"),
|
|
1526
|
+
endTime: toSeconds(end, "endTime"),
|
|
1527
|
+
isAllowlist: false,
|
|
1528
|
+
paymentToken: meta.token,
|
|
1529
|
+
pricePer: config.price.per ?? "unit"
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
async function readWallet(ctx, config, { payer, recipient, quantity, drop }) {
|
|
1533
|
+
validateContractConfig(config);
|
|
1534
|
+
throwIfAborted(ctx);
|
|
1535
|
+
const qty = BigInt(quantity);
|
|
1536
|
+
const unitPrice = drop.unitPrice.value;
|
|
1537
|
+
const totalPrice = totalFor(drop.pricePer, unitPrice, qty);
|
|
1538
|
+
const vars = { payer, recipient, quantity: qty, unitPrice, totalPrice };
|
|
1539
|
+
const [walletMinted, rawMax, native, erc20] = await Promise.all([
|
|
1540
|
+
optField(ctx, config, "walletMinted", config.walletMinted, vars),
|
|
1541
|
+
optField(ctx, config, "walletMax", config.walletMax, vars),
|
|
1542
|
+
io(ctx, () => ctx.client.getBalance({ address: payer })).catch(rpcError),
|
|
1543
|
+
drop.paymentToken ? readCall(ctx, { address: drop.paymentToken, abi: ERC20_ABI2, functionName: "balanceOf", args: [payer] }, "price.currency.address").then(
|
|
1544
|
+
(v) => toBigInt(v, "price.currency.address")
|
|
1545
|
+
) : Promise.resolve(null)
|
|
1546
|
+
]);
|
|
1547
|
+
const walletMax = rawMax === null || rawMax === 0n ? null : rawMax;
|
|
1548
|
+
const remaining = walletMax === null ? null : walletMax > (walletMinted ?? 0n) ? walletMax - (walletMinted ?? 0n) : 0n;
|
|
1549
|
+
const maxQuantity = remaining === null ? null : Number(remaining < BigInt(MAX_QUANTITY) ? remaining : BigInt(MAX_QUANTITY));
|
|
1550
|
+
let ineligible = null;
|
|
1551
|
+
let gas = FALLBACK_MINT_GAS;
|
|
1552
|
+
if (remaining === 0n) {
|
|
1553
|
+
ineligible = { reason: "WALLET_LIMIT", message: `This wallet has already minted its limit of ${walletMax}.` };
|
|
1554
|
+
} else if (drop.paymentToken === null && native >= totalPrice) {
|
|
1555
|
+
const call = { payer, to: getAddress3(config.address), data: encodeMint(config, vars), value: mintValue(config, null, totalPrice) };
|
|
1556
|
+
try {
|
|
1557
|
+
await simulateMint(ctx, config, call);
|
|
1558
|
+
gas = await io(ctx, () => ctx.client.estimateGas({ account: payer, to: call.to, data: call.data, value: call.value })).catch((e) => {
|
|
1559
|
+
throw toSimulationError(e, config.abi);
|
|
1560
|
+
});
|
|
1561
|
+
} catch (e) {
|
|
1562
|
+
if (!isMintError(e) || e.reason !== "SIMULATION_REVERTED") throw e;
|
|
1563
|
+
ineligible = { reason: "SIMULATION_REVERTED", message: e.message };
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if (drop.paymentToken) gas += APPROVE_GAS;
|
|
1567
|
+
const gasPrice = await io(ctx, () => ctx.client.getGasPrice()).catch(rpcError);
|
|
1568
|
+
return {
|
|
1569
|
+
walletMinted,
|
|
1570
|
+
walletMax,
|
|
1571
|
+
allowlistRemaining: null,
|
|
1572
|
+
maxQuantity,
|
|
1573
|
+
ineligible,
|
|
1574
|
+
balances: { native, erc20 },
|
|
1575
|
+
gasReserve: gas * gasPrice
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
async function buildPlan2(ctx, attempt, wallet) {
|
|
1579
|
+
void wallet;
|
|
1580
|
+
const { config, payer, recipient } = attempt;
|
|
1581
|
+
validateContractConfig(config);
|
|
1582
|
+
throwIfAborted(ctx);
|
|
1583
|
+
const qty = BigInt(attempt.quantity);
|
|
1584
|
+
const unitPrice = await resolvePrice(ctx, config);
|
|
1585
|
+
const totalPrice = totalFor(config.price.per ?? "unit", unitPrice, qty);
|
|
1586
|
+
const to = getAddress3(config.address);
|
|
1587
|
+
const token = erc20Of(config);
|
|
1588
|
+
const mint = {
|
|
1589
|
+
to,
|
|
1590
|
+
data: encodeMint(config, { payer, recipient, quantity: qty, unitPrice, totalPrice }),
|
|
1591
|
+
value: mintValue(config, token, totalPrice)
|
|
1592
|
+
};
|
|
1593
|
+
let approve = null;
|
|
1594
|
+
if (token) {
|
|
1595
|
+
const allowance = toBigInt(
|
|
1596
|
+
await readCall(ctx, { address: token, abi: ERC20_ABI2, functionName: "allowance", args: [payer, to] }, "price.currency.address"),
|
|
1597
|
+
"price.currency.address"
|
|
1598
|
+
);
|
|
1599
|
+
if (allowance < totalPrice) {
|
|
1600
|
+
approve = {
|
|
1601
|
+
token,
|
|
1602
|
+
spender: to,
|
|
1603
|
+
amount: totalPrice,
|
|
1604
|
+
tx: { to: token, data: encodeFunctionData2({ abi: ERC20_ABI2, functionName: "approve", args: [to, totalPrice] }), value: 0n }
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return { approve, mint, tokenContract: getAddress3(attempt.tokenContract) };
|
|
1609
|
+
}
|
|
1610
|
+
async function simulate(ctx, attempt, plan) {
|
|
1611
|
+
throwIfAborted(ctx);
|
|
1612
|
+
await simulateMint(ctx, attempt.config, { payer: attempt.payer, to: plan.mint.to, data: plan.mint.data, value: plan.mint.value });
|
|
1613
|
+
}
|
|
1614
|
+
var contractAdapter = { readDrop, readWallet, buildPlan: buildPlan2, simulate };
|
|
1615
|
+
|
|
1616
|
+
// src/core/adapters/receipt.ts
|
|
1617
|
+
import { decodeAbiParameters, getAddress as getAddress4, zeroAddress as zeroAddress2 } from "viem";
|
|
1618
|
+
var TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
1619
|
+
var TRANSFER_SINGLE_TOPIC = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62";
|
|
1620
|
+
var TRANSFER_BATCH_TOPIC = "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb";
|
|
1621
|
+
var topicAddress = (t) => t && t.length === 66 ? getAddress4(`0x${t.slice(26)}`) : null;
|
|
1622
|
+
function parseMintReceipt(logs, { tokenContract, recipient }) {
|
|
1623
|
+
const contract = getAddress4(tokenContract);
|
|
1624
|
+
const to = getAddress4(recipient);
|
|
1625
|
+
const out = /* @__PURE__ */ new Map();
|
|
1626
|
+
const add = (id, amount) => out.set(id, (out.get(id) ?? 0n) + amount);
|
|
1627
|
+
for (const log of logs) {
|
|
1628
|
+
if (getAddress4(log.address) !== contract) continue;
|
|
1629
|
+
const [t0, t1, t2, t3] = log.topics;
|
|
1630
|
+
if (t0 === TRANSFER_TOPIC && log.topics.length === 4) {
|
|
1631
|
+
if (topicAddress(t1) !== zeroAddress2 || topicAddress(t2) !== to || !t3) continue;
|
|
1632
|
+
add(BigInt(t3), 1n);
|
|
1633
|
+
} else if (t0 === TRANSFER_SINGLE_TOPIC && log.topics.length === 4) {
|
|
1634
|
+
if (topicAddress(t2) !== zeroAddress2 || topicAddress(t3) !== to) continue;
|
|
1635
|
+
const [id, value] = decodeAbiParameters([{ type: "uint256" }, { type: "uint256" }], log.data);
|
|
1636
|
+
add(id, value);
|
|
1637
|
+
} else if (t0 === TRANSFER_BATCH_TOPIC && log.topics.length === 4) {
|
|
1638
|
+
if (topicAddress(t2) !== zeroAddress2 || topicAddress(t3) !== to) continue;
|
|
1639
|
+
const [ids, values] = decodeAbiParameters([{ type: "uint256[]" }, { type: "uint256[]" }], log.data);
|
|
1640
|
+
if (ids.length !== values.length) continue;
|
|
1641
|
+
ids.forEach((id, i) => add(id, values[i]));
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return [...out].map(([tokenId, amount]) => ({ contract, tokenId, amount }));
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/core/friendly-errors.ts
|
|
1648
|
+
var GENERIC = "Transaction failed. Please try again. If it fails again, please contact support.";
|
|
1649
|
+
var REVERTED = "Transaction reverted";
|
|
1650
|
+
var record = (value) => value !== null && typeof value === "object" ? value : void 0;
|
|
1651
|
+
var text = (value) => typeof value === "string" ? value.toLowerCase() : "";
|
|
1652
|
+
function collect(error) {
|
|
1653
|
+
const pending = [error];
|
|
1654
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1655
|
+
const errors = [];
|
|
1656
|
+
while (pending.length) {
|
|
1657
|
+
const e = record(pending.pop());
|
|
1658
|
+
if (!e || seen.has(e)) continue;
|
|
1659
|
+
seen.add(e);
|
|
1660
|
+
const data = record(e.data);
|
|
1661
|
+
errors.push({
|
|
1662
|
+
name: e.name,
|
|
1663
|
+
code: e.code,
|
|
1664
|
+
cancelled: e.cancelled,
|
|
1665
|
+
replacement: e.replacement,
|
|
1666
|
+
message: [e.message, e.shortMessage, isMintError(e) ? void 0 : e.reason, data?.message].map(text).join("\n")
|
|
1667
|
+
});
|
|
1668
|
+
pending.push(e.cause, data?.originalError);
|
|
1669
|
+
}
|
|
1670
|
+
return errors;
|
|
1671
|
+
}
|
|
1672
|
+
function fundsMessage(errors) {
|
|
1673
|
+
const hasText = (phrase) => errors.some((e) => e.message.includes(phrase));
|
|
1674
|
+
if (hasText("insufficient funds for gas")) {
|
|
1675
|
+
return "Your wallet does not have enough funds to pay for gas fees. Please try again with a different wallet or add more funds to your wallet.";
|
|
1676
|
+
}
|
|
1677
|
+
if (errors.some((e) => e.name === "InsufficientFundsError" || e.code === "INSUFFICIENT_FUNDS") || hasText("insufficient funds") || hasText("balance too low to proceed")) {
|
|
1678
|
+
return "Your wallet does not have enough funds to complete this transaction. Please try again with a different wallet or add more funds to your wallet.";
|
|
1679
|
+
}
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
function friendlyErrorMessage(error) {
|
|
1683
|
+
if (isMintError(error)) {
|
|
1684
|
+
if (error.reason === "INSUFFICIENT_FUNDS") return fundsMessage(collect(error.cause)) ?? error.message;
|
|
1685
|
+
if (error.reason !== "RPC_ERROR" && error.reason !== "SIMULATION_REVERTED") return error.message;
|
|
1686
|
+
if (error.reason === "SIMULATION_REVERTED" && (error.message === "The token refused the approval." || error.message === "The drop is no longer available for this quantity.")) return error.message;
|
|
1687
|
+
}
|
|
1688
|
+
const errors = collect(error);
|
|
1689
|
+
const hasText = (phrase) => errors.some((e) => e.message.includes(phrase));
|
|
1690
|
+
const hasName = (name) => errors.some((e) => e.name === name);
|
|
1691
|
+
const hasCode2 = (code2) => errors.some((e) => e.code === code2);
|
|
1692
|
+
if (hasName("UserRejectedRequestError") || hasCode2("ACTION_REJECTED") || hasCode2(4001) || hasText("user denied transaction signature") || hasText("userrefusedondevice") || hasText("rejected transaction") || hasText("user rejected") || errors.some((e) => e.message.includes("cancelled") && !e.cancelled)) {
|
|
1693
|
+
return "Transaction Rejected";
|
|
1694
|
+
}
|
|
1695
|
+
if (hasCode2(4902)) return "Network not available in wallet";
|
|
1696
|
+
const replaced = errors.find((e) => e.name === "TransactionReplacedError" || e.code === "TRANSACTION_REPLACED");
|
|
1697
|
+
if (replaced?.cancelled) return "Transaction was cancelled";
|
|
1698
|
+
if (replaced?.replacement) return "Transaction was replaced with a new transaction (usually due to speed up)";
|
|
1699
|
+
if (hasText("ledger")) return "Error with Ledger device. Please ensure device is connected and unlocked";
|
|
1700
|
+
if (errors.some((e) => e.code === -32002 && e.message.includes("pending"))) {
|
|
1701
|
+
return "Transaction already pending in wallet. Please check your wallet";
|
|
1702
|
+
}
|
|
1703
|
+
if (hasText("invalid amount")) return "Price calculation is incorrect, contact support!";
|
|
1704
|
+
const funds = fundsMessage(errors);
|
|
1705
|
+
if (funds) return funds;
|
|
1706
|
+
if (hasName("NonceTooLowError") || hasName("NonceExhaustedError") || hasText("nonce too low")) {
|
|
1707
|
+
return "Transaction nonce is too low. Please try again";
|
|
1708
|
+
}
|
|
1709
|
+
if (hasName("FeeCapTooLowError") || hasText("max fee per gas less than block base fee")) {
|
|
1710
|
+
return "Gas price too low for current network conditions. Please try again";
|
|
1711
|
+
}
|
|
1712
|
+
if (hasName("TimeoutError") || hasCode2("TIMEOUT") || hasText("timeout")) {
|
|
1713
|
+
return "Transaction timed out. Your wallet connection is having issues. Disconnect and reconnect your wallet, then please try again";
|
|
1714
|
+
}
|
|
1715
|
+
if (hasText("network disconnected")) return "Network connection lost. Please check your internet connection and try again";
|
|
1716
|
+
if (hasName("ChainMismatchError") || hasText("wrong network")) return "Wrong network selected. Please switch to the correct network";
|
|
1717
|
+
if (hasText("erc20: transfer amount exceeds balance")) {
|
|
1718
|
+
return "You do not have the required amount of ERC20 tokens to complete this transaction.";
|
|
1719
|
+
}
|
|
1720
|
+
if (hasName("EstimateGasExecutionError") || hasCode2("UNPREDICTABLE_GAS_LIMIT") || hasText("cannot estimate gas")) {
|
|
1721
|
+
return "Unable to estimate gas for transaction. The transaction may fail. Please try again.";
|
|
1722
|
+
}
|
|
1723
|
+
if (hasName("ContractFunctionExecutionError") || hasCode2("CALL_EXCEPTION")) {
|
|
1724
|
+
return "Transaction failed due to contract execution error";
|
|
1725
|
+
}
|
|
1726
|
+
if (hasName("ContractFunctionRevertedError") || hasName("ExecutionRevertedError") || hasText("revert")) return REVERTED;
|
|
1727
|
+
if (hasName("TransactionExecutionError")) return "Transaction execution failed";
|
|
1728
|
+
if (hasCode2("NETWORK_ERROR") || hasName("HttpRequestError") || hasText("network")) return "Network error occurred";
|
|
1729
|
+
return isMintError(error) && error.reason === "SIMULATION_REVERTED" ? REVERTED : GENERIC;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// src/react/useMint.ts
|
|
1733
|
+
var REFRESH_MS = 3e4;
|
|
1734
|
+
var TICK_MS = 1e3;
|
|
1735
|
+
var ASYNC_STATES = ["loading", "connecting", "switching-chain", "approving", "minting", "confirming"];
|
|
1736
|
+
function isAsyncState(name) {
|
|
1737
|
+
return ASYNC_STATES.includes(name);
|
|
1738
|
+
}
|
|
1739
|
+
var nowSec = () => Math.floor(Date.now() / 1e3);
|
|
1740
|
+
function configKeyOf(config) {
|
|
1741
|
+
return JSON.stringify(config, (_k, v) => typeof v === "bigint" ? { __dropkitBigInt: v.toString() } : v);
|
|
1742
|
+
}
|
|
1743
|
+
function adapterFor(config) {
|
|
1744
|
+
return config.type === "manifold-claim" ? manifoldClaimAdapter : contractAdapter;
|
|
1745
|
+
}
|
|
1746
|
+
var bridgeEnabled = (config) => config.type === "manifold-claim";
|
|
1747
|
+
function tokenContractOf(config) {
|
|
1748
|
+
return config.type === "manifold-claim" ? config.creatorContract : config.tokenContract ?? config.address;
|
|
1749
|
+
}
|
|
1750
|
+
function eventAddressOf(config) {
|
|
1751
|
+
return config.type === "manifold-claim" ? config.creatorContract : config.address;
|
|
1752
|
+
}
|
|
1753
|
+
var isUnsupported = (reason) => UNSUPPORTED_REASONS.includes(reason);
|
|
1754
|
+
function isRejection(error) {
|
|
1755
|
+
for (let e = error, depth = 0; e && depth < 8; depth++) {
|
|
1756
|
+
if (e.code === 4001) return true;
|
|
1757
|
+
e = e.cause;
|
|
1758
|
+
}
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1761
|
+
function toReason(e) {
|
|
1762
|
+
if (isMintError(e)) return { reason: e.reason, message: friendlyErrorMessage(e) };
|
|
1763
|
+
if (isRejection(e)) return { reason: "USER_REJECTED", message: "You rejected the request." };
|
|
1764
|
+
for (let cause = e, depth = 0; cause && depth < 8; depth++) {
|
|
1765
|
+
if (cause.name === "ChainMismatchError") return { reason: "CHAIN_SWITCH_FAILED", message: "The wallet network changed before the transaction was sent." };
|
|
1766
|
+
cause = cause.cause;
|
|
1767
|
+
}
|
|
1768
|
+
return { reason: "RPC_ERROR", message: friendlyErrorMessage(e) };
|
|
1769
|
+
}
|
|
1770
|
+
function errorReasonOf(e) {
|
|
1771
|
+
const { reason, message } = toReason(e);
|
|
1772
|
+
const ok = ["USER_REJECTED", "TX_REVERTED", "RATE_LIMITED", "RPC_ERROR", "CHAIN_SWITCH_FAILED", "ALLOWLIST_UNAVAILABLE"];
|
|
1773
|
+
return { reason: ok.includes(reason) ? reason : "RPC_ERROR", message };
|
|
1774
|
+
}
|
|
1775
|
+
function explorerUrlFor(chainId, hash) {
|
|
1776
|
+
try {
|
|
1777
|
+
const url = getChain(chainId).blockExplorers?.default.url;
|
|
1778
|
+
return url ? `${url.replace(/\/$/, "")}/tx/${hash}` : null;
|
|
1779
|
+
} catch {
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
var receiptOk = (status) => status === "success" || status === 1 || status === 1n || status === "0x1";
|
|
1784
|
+
function useMint(options) {
|
|
1785
|
+
const { config, recipient, quantity: quantityProp, connect } = options;
|
|
1786
|
+
const [machine, rawDispatch] = useReducer(reducer, void 0, () => initialState({ quantity: quantityProp }));
|
|
1787
|
+
const [widgetFail, setWidgetFail] = useState(null);
|
|
1788
|
+
const [widgetRetry, setWidgetRetry] = useState(0);
|
|
1789
|
+
const configKey = configKeyOf(config);
|
|
1790
|
+
const mounted = useRef(false);
|
|
1791
|
+
const stateRef = useRef(machine);
|
|
1792
|
+
const propsRef = useRef(options);
|
|
1793
|
+
const lifeRef = useRef(null);
|
|
1794
|
+
const genRef = useRef(0);
|
|
1795
|
+
const attemptRef = useRef(null);
|
|
1796
|
+
const attemptIdRef = useRef(0);
|
|
1797
|
+
const accountKeyRef = useRef(null);
|
|
1798
|
+
const connectingRef = useRef(false);
|
|
1799
|
+
const pendingRefreshRef = useRef(false);
|
|
1800
|
+
const pendingQuantityRef = useRef(null);
|
|
1801
|
+
const widgetFailRef = useRef(null);
|
|
1802
|
+
const widgetLoadRef = useRef(null);
|
|
1803
|
+
const pushAccountRef = useRef(null);
|
|
1804
|
+
useEffect(() => {
|
|
1805
|
+
stateRef.current = machine;
|
|
1806
|
+
propsRef.current = options;
|
|
1807
|
+
widgetFailRef.current = widgetFail;
|
|
1808
|
+
});
|
|
1809
|
+
const dispatch = useCallback((event) => {
|
|
1810
|
+
if (!mounted.current) return;
|
|
1811
|
+
stateRef.current = reducer(stateRef.current, event);
|
|
1812
|
+
rawDispatch(event);
|
|
1813
|
+
}, []);
|
|
1814
|
+
const emit = useCallback((type, over = {}) => {
|
|
1815
|
+
const handler = propsRef.current.onEvent;
|
|
1816
|
+
if (!handler || !mounted.current) return;
|
|
1817
|
+
const cfg = attemptRef.current?.config ?? propsRef.current.config;
|
|
1818
|
+
const s = stateRef.current;
|
|
1819
|
+
const payload = {
|
|
1820
|
+
type,
|
|
1821
|
+
state: s.name,
|
|
1822
|
+
reason: s.reason,
|
|
1823
|
+
chainId: cfg.chainId,
|
|
1824
|
+
quantity: s.quantity,
|
|
1825
|
+
config: { type: cfg.type, chainId: cfg.chainId, address: eventAddressOf(cfg) },
|
|
1826
|
+
...over
|
|
1827
|
+
};
|
|
1828
|
+
try {
|
|
1829
|
+
handler(payload);
|
|
1830
|
+
} catch {
|
|
1831
|
+
}
|
|
1832
|
+
}, []);
|
|
1833
|
+
const clientFor = useCallback((life, cfg) => {
|
|
1834
|
+
const rpcUrl = propsRef.current.rpcUrls?.[cfg.chainId];
|
|
1835
|
+
if (!life.client || life.rpcUrl !== rpcUrl) {
|
|
1836
|
+
life.rpcUrl = rpcUrl;
|
|
1837
|
+
life.client = createReadClient({ chainId: cfg.chainId, rpcUrls: propsRef.current.rpcUrls, useBridge: bridgeEnabled(cfg) });
|
|
1838
|
+
}
|
|
1839
|
+
return life.client;
|
|
1840
|
+
}, []);
|
|
1841
|
+
const ctxFor = useCallback(
|
|
1842
|
+
(life, cfg) => ({
|
|
1843
|
+
client: clientFor(life, cfg),
|
|
1844
|
+
cacheScope: life.cacheScope,
|
|
1845
|
+
// The lifetime signal: a config change or unmount cancels every adapter call, including shared merkle fetches.
|
|
1846
|
+
signal: life.controller.signal
|
|
1847
|
+
}),
|
|
1848
|
+
[clientFor]
|
|
1849
|
+
);
|
|
1850
|
+
const readDrop2 = useCallback(
|
|
1851
|
+
async (life, gen, cfg) => {
|
|
1852
|
+
const adapter = adapterFor(cfg);
|
|
1853
|
+
try {
|
|
1854
|
+
const drop = await adapter.readDrop(ctxFor(life, cfg), cfg);
|
|
1855
|
+
if (life.controller.signal.aborted || life.gen !== gen || lifeRef.current !== life) return;
|
|
1856
|
+
dispatch({ type: "DROP_LOADED", gen, drop, now: Math.max(stateRef.current.now, nowSec()) });
|
|
1857
|
+
} catch (e) {
|
|
1858
|
+
if (life.controller.signal.aborted || life.gen !== gen || lifeRef.current !== life) return;
|
|
1859
|
+
const { reason, message } = toReason(e);
|
|
1860
|
+
if (isUnsupported(reason)) dispatch({ type: "DROP_UNSUPPORTED", gen, reason, message });
|
|
1861
|
+
else dispatch({ type: "READ_FAILED", gen, ...errorReasonOf(e) });
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
[ctxFor, dispatch]
|
|
1865
|
+
);
|
|
1866
|
+
const startLifetime = useCallback(
|
|
1867
|
+
(key, cfg) => {
|
|
1868
|
+
const prev = lifeRef.current;
|
|
1869
|
+
lifeRef.current = null;
|
|
1870
|
+
prev?.controller.abort();
|
|
1871
|
+
const gen = ++genRef.current;
|
|
1872
|
+
const life = { key, controller: new AbortController(), cacheScope: {}, gen };
|
|
1873
|
+
lifeRef.current = life;
|
|
1874
|
+
dispatch({ type: "LOAD", gen, kind: "config" });
|
|
1875
|
+
void readDrop2(life, gen, cfg);
|
|
1876
|
+
},
|
|
1877
|
+
[dispatch, readDrop2]
|
|
1878
|
+
);
|
|
1879
|
+
const refreshNow = useCallback(
|
|
1880
|
+
async (kind = "refresh") => {
|
|
1881
|
+
const life = lifeRef.current;
|
|
1882
|
+
if (!life || !mounted.current) return;
|
|
1883
|
+
if (connectingRef.current || attemptRef.current || stateRef.current.attempt) {
|
|
1884
|
+
pendingRefreshRef.current = true;
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
const gen = ++genRef.current;
|
|
1888
|
+
life.gen = gen;
|
|
1889
|
+
dispatch({ type: "LOAD", gen, kind });
|
|
1890
|
+
await readDrop2(life, gen, propsRef.current.config);
|
|
1891
|
+
},
|
|
1892
|
+
[dispatch, readDrop2]
|
|
1893
|
+
);
|
|
1894
|
+
const refresh = useCallback(() => refreshNow("refresh"), [refreshNow]);
|
|
1895
|
+
const isBusy = useCallback(
|
|
1896
|
+
() => connectingRef.current || attemptRef.current !== null || stateRef.current.attempt !== null || IN_FLIGHT_STATES.includes(stateRef.current.name),
|
|
1897
|
+
[]
|
|
1898
|
+
);
|
|
1899
|
+
const executeAttempt = useCallback(
|
|
1900
|
+
async (attempt, walletRead, life, account2) => {
|
|
1901
|
+
const cfg = attempt.config;
|
|
1902
|
+
const adapter = adapterFor(cfg);
|
|
1903
|
+
const ctx = ctxFor(life, cfg);
|
|
1904
|
+
const client = clientFor(life, cfg);
|
|
1905
|
+
const assertActive = () => {
|
|
1906
|
+
if (!mounted.current || life.controller.signal.aborted || lifeRef.current !== life || attemptRef.current !== attempt) {
|
|
1907
|
+
throw new DOMException("Mint attempt cancelled", "AbortError");
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
const assertPayer = () => {
|
|
1911
|
+
assertActive();
|
|
1912
|
+
const live = getAccount();
|
|
1913
|
+
if (!live || live.address.toLowerCase() !== attempt.payer.toLowerCase()) {
|
|
1914
|
+
throw new MintError("USER_REJECTED", "The connected wallet changed before the transaction was sent.");
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
let wallet2 = null;
|
|
1918
|
+
const walletFor = () => {
|
|
1919
|
+
if (!wallet2) wallet2 = getWalletClient(attempt.chainId);
|
|
1920
|
+
return wallet2;
|
|
1921
|
+
};
|
|
1922
|
+
const submit = async (tx) => {
|
|
1923
|
+
assertPayer();
|
|
1924
|
+
const w = walletFor();
|
|
1925
|
+
return w.sendTransaction({ account: attempt.payer, to: tx.to, data: tx.data, value: tx.value, chain: getChain(attempt.chainId) });
|
|
1926
|
+
};
|
|
1927
|
+
const waitReceipt = async (hash2) => {
|
|
1928
|
+
let cancelled = false;
|
|
1929
|
+
const params = {
|
|
1930
|
+
hash: hash2,
|
|
1931
|
+
confirmations: 1,
|
|
1932
|
+
timeout: 12e4,
|
|
1933
|
+
onReplaced: ({ reason }) => {
|
|
1934
|
+
if (reason !== "repriced") cancelled = true;
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
let receipt2;
|
|
1938
|
+
try {
|
|
1939
|
+
receipt2 = await client.waitForTransactionReceipt(params);
|
|
1940
|
+
} catch (e) {
|
|
1941
|
+
assertActive();
|
|
1942
|
+
if (cancelled) throw new MintError("USER_REJECTED", "The transaction was cancelled or replaced with a different transaction.");
|
|
1943
|
+
if (await walletFor().getChainId() !== attempt.chainId) throw e;
|
|
1944
|
+
assertActive();
|
|
1945
|
+
receipt2 = await walletFor().extend(publicActions).waitForTransactionReceipt(params);
|
|
1946
|
+
}
|
|
1947
|
+
assertActive();
|
|
1948
|
+
if (cancelled) throw new MintError("USER_REJECTED", "The transaction was cancelled or replaced with a different transaction.");
|
|
1949
|
+
return receipt2;
|
|
1950
|
+
};
|
|
1951
|
+
assertPayer();
|
|
1952
|
+
if (account2.chainId !== attempt.chainId) {
|
|
1953
|
+
dispatch({ type: "SWITCHING_CHAIN", attemptId: attempt.attemptId });
|
|
1954
|
+
await switchToChain(attempt.chainId);
|
|
1955
|
+
}
|
|
1956
|
+
assertPayer();
|
|
1957
|
+
let plan = await adapter.buildPlan(ctx, attempt, walletRead);
|
|
1958
|
+
assertPayer();
|
|
1959
|
+
if (plan.approve) {
|
|
1960
|
+
const a = plan.approve;
|
|
1961
|
+
try {
|
|
1962
|
+
const simulated = await client.call({ account: attempt.payer, to: a.tx.to, data: a.tx.data, value: a.tx.value });
|
|
1963
|
+
if (simulated.data && simulated.data !== "0x" && BigInt(simulated.data) === 0n) {
|
|
1964
|
+
throw new MintError("SIMULATION_REVERTED", "The token refused the approval.");
|
|
1965
|
+
}
|
|
1966
|
+
} catch (e) {
|
|
1967
|
+
if (isMintError(e)) throw e;
|
|
1968
|
+
const reverted = e instanceof BaseError3 ? e.walk((cause) => cause instanceof ExecutionRevertedError) : null;
|
|
1969
|
+
if (reverted instanceof ExecutionRevertedError) {
|
|
1970
|
+
throw new MintError("SIMULATION_REVERTED", reverted.shortMessage);
|
|
1971
|
+
}
|
|
1972
|
+
throw e;
|
|
1973
|
+
}
|
|
1974
|
+
assertPayer();
|
|
1975
|
+
dispatch({ type: "APPROVING", attemptId: attempt.attemptId });
|
|
1976
|
+
const approveHash = await submit(a.tx);
|
|
1977
|
+
assertActive();
|
|
1978
|
+
emit("approve-submitted", { state: "approving", reason: null, txHash: approveHash, quantity: attempt.quantity, chainId: attempt.chainId });
|
|
1979
|
+
const approveReceipt = await waitReceipt(approveHash);
|
|
1980
|
+
assertActive();
|
|
1981
|
+
if (!receiptOk(approveReceipt.status)) {
|
|
1982
|
+
throw new MintError("TX_REVERTED", `The approval transaction reverted (${approveHash}).`);
|
|
1983
|
+
}
|
|
1984
|
+
const freshDrop = await adapter.readDrop(ctx, cfg);
|
|
1985
|
+
assertActive();
|
|
1986
|
+
const freshWallet = await adapter.readWallet(ctx, cfg, {
|
|
1987
|
+
payer: attempt.payer,
|
|
1988
|
+
recipient: attempt.recipient,
|
|
1989
|
+
quantity: attempt.quantity,
|
|
1990
|
+
drop: freshDrop
|
|
1991
|
+
});
|
|
1992
|
+
assertPayer();
|
|
1993
|
+
let checked = reducer(initialState({ quantity: attempt.quantity }), { type: "ACCOUNT_CHANGED", account: account2 });
|
|
1994
|
+
checked = reducer(checked, { type: "DROP_LOADED", gen: checked.readGen, drop: freshDrop, now: nowSec() });
|
|
1995
|
+
checked = reducer(checked, { type: "WALLET_LOADED", gen: checked.walletGen, read: freshWallet });
|
|
1996
|
+
if (checked.name !== "live" || (selectMaxQuantity(checked) ?? 0) < attempt.quantity) {
|
|
1997
|
+
throw new MintError(checked.reason ?? "SIMULATION_REVERTED", checked.message ?? "The drop is no longer available for this quantity.");
|
|
1998
|
+
}
|
|
1999
|
+
plan = await adapter.buildPlan(ctx, attempt, freshWallet);
|
|
2000
|
+
assertPayer();
|
|
2001
|
+
}
|
|
2002
|
+
await adapter.simulate(ctx, attempt, plan);
|
|
2003
|
+
assertPayer();
|
|
2004
|
+
dispatch({ type: "MINT_PROMPTED", attemptId: attempt.attemptId });
|
|
2005
|
+
const hash = await submit(plan.mint);
|
|
2006
|
+
assertActive();
|
|
2007
|
+
dispatch({ type: "TX_SUBMITTED", attemptId: attempt.attemptId });
|
|
2008
|
+
emit("tx-submitted", { state: "confirming", reason: null, txHash: hash, quantity: attempt.quantity, chainId: attempt.chainId });
|
|
2009
|
+
const receipt = await waitReceipt(hash);
|
|
2010
|
+
assertActive();
|
|
2011
|
+
const confirmedHash = receipt.transactionHash ?? hash;
|
|
2012
|
+
if (!receiptOk(receipt.status)) {
|
|
2013
|
+
throw new MintError("TX_REVERTED", `The mint transaction reverted (${confirmedHash}).`);
|
|
2014
|
+
}
|
|
2015
|
+
const tokens = parseMintReceipt(receipt.logs ?? [], {
|
|
2016
|
+
tokenContract: plan.tokenContract ?? attempt.tokenContract,
|
|
2017
|
+
recipient: attempt.recipient
|
|
2018
|
+
});
|
|
2019
|
+
const result = { txHash: confirmedHash, explorerUrl: explorerUrlFor(attempt.chainId, confirmedHash), tokens };
|
|
2020
|
+
dispatch({ type: "ATTEMPT_SUCCEEDED", attemptId: attempt.attemptId, result });
|
|
2021
|
+
emit("success", { state: "success", reason: null, txHash: confirmedHash, quantity: attempt.quantity, chainId: attempt.chainId });
|
|
2022
|
+
},
|
|
2023
|
+
[clientFor, ctxFor, dispatch, emit]
|
|
2024
|
+
);
|
|
2025
|
+
const runMint = useCallback(async () => {
|
|
2026
|
+
if (!mounted.current) return;
|
|
2027
|
+
if (attemptRef.current !== null) return;
|
|
2028
|
+
if (widgetFailRef.current) {
|
|
2029
|
+
if (isUnsupported(widgetFailRef.current.reason)) return;
|
|
2030
|
+
widgetFailRef.current = null;
|
|
2031
|
+
setWidgetFail(null);
|
|
2032
|
+
setWidgetRetry((n) => n + 1);
|
|
2033
|
+
void refreshNow("config");
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
const s = stateRef.current;
|
|
2037
|
+
if (!canAct(s) || s.attempt !== null) return;
|
|
2038
|
+
if (s.name === "disconnected") {
|
|
2039
|
+
if (connectingRef.current) return;
|
|
2040
|
+
connectingRef.current = true;
|
|
2041
|
+
dispatch({ type: "CONNECT_STARTED" });
|
|
2042
|
+
const connectLife = lifeRef.current;
|
|
2043
|
+
const active = () => mounted.current && lifeRef.current === connectLife && !connectLife?.controller.signal.aborted;
|
|
2044
|
+
try {
|
|
2045
|
+
await widgetLoadRef.current;
|
|
2046
|
+
if (!active()) return;
|
|
2047
|
+
const outcome = await openConnectModal();
|
|
2048
|
+
if (!active()) return;
|
|
2049
|
+
connectingRef.current = false;
|
|
2050
|
+
if (outcome === "connected") {
|
|
2051
|
+
accountKeyRef.current = null;
|
|
2052
|
+
pushAccountRef.current?.(getAccount());
|
|
2053
|
+
} else dispatch({ type: "CONNECT_DISMISSED" });
|
|
2054
|
+
} catch {
|
|
2055
|
+
if (active()) dispatch({ type: "CONNECT_DISMISSED" });
|
|
2056
|
+
} finally {
|
|
2057
|
+
connectingRef.current = false;
|
|
2058
|
+
}
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
if (s.name === "success" || s.name === "error") {
|
|
2062
|
+
const life2 = lifeRef.current;
|
|
2063
|
+
const gen = ++genRef.current;
|
|
2064
|
+
if (life2) life2.gen = gen;
|
|
2065
|
+
dispatch({ type: "RESET", gen });
|
|
2066
|
+
dispatch({ type: "LOAD", gen, kind: "config" });
|
|
2067
|
+
if (life2) void readDrop2(life2, gen, propsRef.current.config);
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
if (s.name !== "live") return;
|
|
2071
|
+
const account2 = s.account;
|
|
2072
|
+
const walletRead = s.walletRead;
|
|
2073
|
+
const life = lifeRef.current;
|
|
2074
|
+
if (!account2 || !walletRead || !life) return;
|
|
2075
|
+
const cfg = propsRef.current.config;
|
|
2076
|
+
const attempt = Object.freeze({
|
|
2077
|
+
attemptId: ++attemptIdRef.current,
|
|
2078
|
+
config: structuredClone(cfg),
|
|
2079
|
+
payer: account2.address,
|
|
2080
|
+
recipient: propsRef.current.recipient ?? account2.address,
|
|
2081
|
+
quantity: s.quantity,
|
|
2082
|
+
chainId: cfg.chainId,
|
|
2083
|
+
tokenContract: tokenContractOf(cfg)
|
|
2084
|
+
});
|
|
2085
|
+
attemptRef.current = attempt;
|
|
2086
|
+
dispatch({ type: "ATTEMPT_STARTED", attempt });
|
|
2087
|
+
emit("attempt", { state: "live", reason: null, quantity: attempt.quantity, chainId: attempt.chainId });
|
|
2088
|
+
try {
|
|
2089
|
+
await executeAttempt(attempt, walletRead, life, account2);
|
|
2090
|
+
} catch (e) {
|
|
2091
|
+
if (!mounted.current || life.controller.signal.aborted || lifeRef.current !== life) return;
|
|
2092
|
+
const { reason, message } = toReason(e);
|
|
2093
|
+
dispatch({ type: "ATTEMPT_FAILED", attemptId: attempt.attemptId, reason, message });
|
|
2094
|
+
emit("error", { quantity: attempt.quantity, chainId: attempt.chainId });
|
|
2095
|
+
} finally {
|
|
2096
|
+
attemptRef.current = null;
|
|
2097
|
+
}
|
|
2098
|
+
}, [dispatch, emit, readDrop2, executeAttempt, refreshNow]);
|
|
2099
|
+
const mint = useCallback(() => {
|
|
2100
|
+
void runMint();
|
|
2101
|
+
}, [runMint]);
|
|
2102
|
+
useEffect(() => {
|
|
2103
|
+
mounted.current = true;
|
|
2104
|
+
return () => {
|
|
2105
|
+
mounted.current = false;
|
|
2106
|
+
lifeRef.current?.controller.abort();
|
|
2107
|
+
lifeRef.current = null;
|
|
2108
|
+
};
|
|
2109
|
+
}, []);
|
|
2110
|
+
useEffect(() => {
|
|
2111
|
+
const push = (a) => {
|
|
2112
|
+
const key = a ? `${a.address.toLowerCase()}:${a.chainId}:${(propsRef.current.recipient ?? "").toLowerCase()}` : "null";
|
|
2113
|
+
if (key === accountKeyRef.current) return;
|
|
2114
|
+
accountKeyRef.current = key;
|
|
2115
|
+
if (attemptRef.current || stateRef.current.attempt) pendingRefreshRef.current = true;
|
|
2116
|
+
dispatch({ type: "ACCOUNT_CHANGED", account: a });
|
|
2117
|
+
};
|
|
2118
|
+
pushAccountRef.current = push;
|
|
2119
|
+
push(getAccount());
|
|
2120
|
+
const unsubscribe = subscribeAccount(push);
|
|
2121
|
+
return () => {
|
|
2122
|
+
unsubscribe();
|
|
2123
|
+
};
|
|
2124
|
+
}, [dispatch]);
|
|
2125
|
+
const recipientRef = useRef(recipient);
|
|
2126
|
+
useEffect(() => {
|
|
2127
|
+
if (isBusy() || recipientRef.current === recipient) return;
|
|
2128
|
+
recipientRef.current = recipient;
|
|
2129
|
+
accountKeyRef.current = null;
|
|
2130
|
+
pushAccountRef.current?.(getAccount());
|
|
2131
|
+
}, [recipient, machine.attempt, isBusy]);
|
|
2132
|
+
const projectId = connect?.walletConnectProjectId;
|
|
2133
|
+
useEffect(() => {
|
|
2134
|
+
let alive = true;
|
|
2135
|
+
const loading = loadConnectWidget({ chainIds: [config.chainId], ...projectId ? { walletConnectProjectId: projectId } : {} });
|
|
2136
|
+
widgetLoadRef.current = loading;
|
|
2137
|
+
loading.then(
|
|
2138
|
+
() => {
|
|
2139
|
+
if (alive) setWidgetFail(null);
|
|
2140
|
+
},
|
|
2141
|
+
(e) => {
|
|
2142
|
+
if (!alive) return;
|
|
2143
|
+
const { reason, message } = toReason(e);
|
|
2144
|
+
setWidgetFail({ reason, message });
|
|
2145
|
+
}
|
|
2146
|
+
);
|
|
2147
|
+
return () => {
|
|
2148
|
+
alive = false;
|
|
2149
|
+
};
|
|
2150
|
+
}, [config.chainId, projectId, widgetRetry]);
|
|
2151
|
+
useEffect(() => {
|
|
2152
|
+
if (!mounted.current) return;
|
|
2153
|
+
if (isBusy()) return;
|
|
2154
|
+
if (pendingQuantityRef.current !== null) {
|
|
2155
|
+
const quantity = pendingQuantityRef.current;
|
|
2156
|
+
pendingQuantityRef.current = null;
|
|
2157
|
+
dispatch({ type: "SET_QUANTITY", quantity });
|
|
2158
|
+
}
|
|
2159
|
+
if (lifeRef.current?.key !== configKey) {
|
|
2160
|
+
pendingRefreshRef.current = false;
|
|
2161
|
+
startLifetime(configKey, config);
|
|
2162
|
+
} else if (pendingRefreshRef.current || lifeRef.current.rpcUrl !== options.rpcUrls?.[config.chainId]) {
|
|
2163
|
+
pendingRefreshRef.current = false;
|
|
2164
|
+
void refreshNow();
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
const previousQuantity = useRef(machine.quantity);
|
|
2168
|
+
useEffect(() => {
|
|
2169
|
+
if (previousQuantity.current === machine.quantity || isBusy()) return;
|
|
2170
|
+
previousQuantity.current = machine.quantity;
|
|
2171
|
+
if (stateRef.current.walletRead) dispatch({ type: "ACCOUNT_CHANGED", account: stateRef.current.account });
|
|
2172
|
+
}, [machine.quantity, machine.attempt, dispatch, isBusy]);
|
|
2173
|
+
const loadedDrop = machine.drop;
|
|
2174
|
+
const walletGen = machine.walletGen;
|
|
2175
|
+
const account = machine.account;
|
|
2176
|
+
const lastWalletRead = useRef(null);
|
|
2177
|
+
useEffect(() => {
|
|
2178
|
+
if (!account || !loadedDrop || isBusy()) return;
|
|
2179
|
+
const previous = lastWalletRead.current;
|
|
2180
|
+
if (stateRef.current.blocked && previous?.drop === loadedDrop && previous.gen === walletGen && previous.quantity === machine.quantity) return;
|
|
2181
|
+
lastWalletRead.current = { drop: loadedDrop, gen: walletGen, quantity: machine.quantity };
|
|
2182
|
+
const life = lifeRef.current;
|
|
2183
|
+
const drop = stateRef.current.drop;
|
|
2184
|
+
if (!life || !drop || life.key !== configKeyOf(propsRef.current.config)) return;
|
|
2185
|
+
const readGen = life.gen;
|
|
2186
|
+
const cfg = propsRef.current.config;
|
|
2187
|
+
const adapter = adapterFor(cfg);
|
|
2188
|
+
let alive = true;
|
|
2189
|
+
void (async () => {
|
|
2190
|
+
try {
|
|
2191
|
+
const read = await adapter.readWallet(ctxFor(life, cfg), cfg, {
|
|
2192
|
+
payer: account.address,
|
|
2193
|
+
recipient: propsRef.current.recipient ?? account.address,
|
|
2194
|
+
quantity: stateRef.current.quantity,
|
|
2195
|
+
drop
|
|
2196
|
+
});
|
|
2197
|
+
if (!alive || life.controller.signal.aborted || lifeRef.current !== life || life.gen !== readGen) return;
|
|
2198
|
+
const ineligible = read.ineligible?.reason === "SIMULATION_REVERTED" ? { ...read.ineligible, message: friendlyErrorMessage(new MintError("SIMULATION_REVERTED", read.ineligible.message)) } : read.ineligible;
|
|
2199
|
+
dispatch({ type: "WALLET_LOADED", gen: walletGen, read: { ...read, ineligible } });
|
|
2200
|
+
} catch (e) {
|
|
2201
|
+
if (!alive || life.controller.signal.aborted || lifeRef.current !== life || life.gen !== readGen) return;
|
|
2202
|
+
dispatch({ type: "WALLET_FAILED", gen: walletGen, ...errorReasonOf(e) });
|
|
2203
|
+
}
|
|
2204
|
+
})();
|
|
2205
|
+
return () => {
|
|
2206
|
+
alive = false;
|
|
2207
|
+
};
|
|
2208
|
+
}, [account, loadedDrop, walletGen, machine.quantity, machine.attempt, ctxFor, dispatch, isBusy]);
|
|
2209
|
+
const prevQuantityProp = useRef(quantityProp);
|
|
2210
|
+
useEffect(() => {
|
|
2211
|
+
if (isBusy()) return;
|
|
2212
|
+
const propChanged = prevQuantityProp.current !== quantityProp;
|
|
2213
|
+
prevQuantityProp.current = quantityProp;
|
|
2214
|
+
if (quantityProp === void 0 || !propChanged && !propsRef.current.onQuantityChange) return;
|
|
2215
|
+
const state = stateRef.current;
|
|
2216
|
+
if (!propChanged && state.account && !state.walletRead) return;
|
|
2217
|
+
const event = { type: "SET_QUANTITY", quantity: quantityProp };
|
|
2218
|
+
const clamped = reducer(state, event).quantity;
|
|
2219
|
+
if (clamped !== state.quantity) dispatch(event);
|
|
2220
|
+
if (clamped !== quantityProp) propsRef.current.onQuantityChange?.(clamped);
|
|
2221
|
+
}, [quantityProp, machine.quantity, machine.drop, machine.walletRead, machine.attempt, dispatch, isBusy]);
|
|
2222
|
+
useEffect(() => {
|
|
2223
|
+
const timer = setInterval(() => {
|
|
2224
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
2225
|
+
void refreshNow("refresh");
|
|
2226
|
+
}, REFRESH_MS);
|
|
2227
|
+
const onVisible = () => {
|
|
2228
|
+
if (document.visibilityState === "visible") void refreshNow("refresh");
|
|
2229
|
+
};
|
|
2230
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
2231
|
+
return () => {
|
|
2232
|
+
clearInterval(timer);
|
|
2233
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
2234
|
+
};
|
|
2235
|
+
}, [refreshNow]);
|
|
2236
|
+
const boundary = machine.drop ? [machine.drop.startTime, machine.drop.endTime].some((t) => t !== null && t > machine.now) : false;
|
|
2237
|
+
useEffect(() => {
|
|
2238
|
+
if (!boundary) return;
|
|
2239
|
+
const timer = setInterval(() => {
|
|
2240
|
+
const s = stateRef.current;
|
|
2241
|
+
const now = nowSec();
|
|
2242
|
+
const crossed = s.drop && [s.drop.startTime, s.drop.endTime].some((t) => t !== null && t > s.now && t <= now);
|
|
2243
|
+
dispatch({ type: "TICK", now });
|
|
2244
|
+
if (crossed) void refreshNow();
|
|
2245
|
+
}, TICK_MS);
|
|
2246
|
+
return () => clearInterval(timer);
|
|
2247
|
+
}, [boundary, dispatch, refreshNow]);
|
|
2248
|
+
const prevName = useRef(null);
|
|
2249
|
+
useEffect(() => {
|
|
2250
|
+
const name = widgetFail ? isUnsupported(widgetFail.reason) ? "unsupported" : "error" : machine.name;
|
|
2251
|
+
if (prevName.current === name) return;
|
|
2252
|
+
prevName.current = name;
|
|
2253
|
+
emit("state", { state: name, reason: widgetFail?.reason ?? machine.reason });
|
|
2254
|
+
}, [machine.name, machine.reason, widgetFail, emit]);
|
|
2255
|
+
const setQuantity = useCallback(
|
|
2256
|
+
(next) => {
|
|
2257
|
+
const s = stateRef.current;
|
|
2258
|
+
const max = selectMaxQuantity(s) ?? 65535;
|
|
2259
|
+
const clamped = Math.max(1, Math.min(Math.floor(Number.isNaN(next) ? 1 : next), max));
|
|
2260
|
+
const handler = propsRef.current.onQuantityChange;
|
|
2261
|
+
if (handler) {
|
|
2262
|
+
handler(clamped);
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
if (attemptRef.current) {
|
|
2266
|
+
pendingQuantityRef.current = clamped;
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
dispatch({ type: "SET_QUANTITY", quantity: clamped });
|
|
2270
|
+
if (s.quantity !== stateRef.current.quantity) {
|
|
2271
|
+
dispatch({ type: "ACCOUNT_CHANGED", account: stateRef.current.account });
|
|
2272
|
+
}
|
|
2273
|
+
},
|
|
2274
|
+
[dispatch]
|
|
2275
|
+
);
|
|
2276
|
+
const effective = widgetFail ? { name: isUnsupported(widgetFail.reason) ? "unsupported" : "error", reason: widgetFail.reason, message: widgetFail.message } : { name: machine.name, reason: machine.reason, message: machine.message };
|
|
2277
|
+
const wallet = machine.account && machine.walletRead ? {
|
|
2278
|
+
address: machine.account.address,
|
|
2279
|
+
chainId: machine.account.chainId,
|
|
2280
|
+
recipient: recipient ?? machine.account.address,
|
|
2281
|
+
walletMinted: machine.walletRead.walletMinted,
|
|
2282
|
+
walletMax: machine.walletRead.walletMax,
|
|
2283
|
+
allowlistRemaining: machine.walletRead.allowlistRemaining
|
|
2284
|
+
} : null;
|
|
2285
|
+
return {
|
|
2286
|
+
state: effective.name,
|
|
2287
|
+
reason: effective.reason,
|
|
2288
|
+
message: effective.message,
|
|
2289
|
+
drop: machine.drop,
|
|
2290
|
+
wallet,
|
|
2291
|
+
quantity: machine.quantity,
|
|
2292
|
+
setQuantity,
|
|
2293
|
+
maxQuantity: selectMaxQuantity(machine),
|
|
2294
|
+
total: selectTotal(machine),
|
|
2295
|
+
mint,
|
|
2296
|
+
result: machine.result,
|
|
2297
|
+
refresh,
|
|
2298
|
+
// An attempt claimed by a click is not clickable, even while the reducer still reads 'live'.
|
|
2299
|
+
canAct: canAct({ ...machine, name: effective.name }) && machine.attempt === null
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// src/react/MintButton.ts
|
|
2304
|
+
import { createElement } from "react";
|
|
2305
|
+
function defaultLabel(m) {
|
|
2306
|
+
const symbol = m.total?.erc20?.symbol ?? m.total?.native.symbol ?? m.drop?.unitPrice.symbol ?? "";
|
|
2307
|
+
switch (m.state) {
|
|
2308
|
+
case "loading":
|
|
2309
|
+
return "Loading\u2026";
|
|
2310
|
+
case "unsupported":
|
|
2311
|
+
return "Unavailable";
|
|
2312
|
+
case "upcoming": {
|
|
2313
|
+
const t = m.drop?.startTime;
|
|
2314
|
+
return `Starts ${t ? new Date(t * 1e3).toLocaleString() : "soon"}`;
|
|
2315
|
+
}
|
|
2316
|
+
case "ended":
|
|
2317
|
+
return "Ended";
|
|
2318
|
+
case "sold-out":
|
|
2319
|
+
return "Sold out";
|
|
2320
|
+
case "disconnected":
|
|
2321
|
+
return "Connect wallet";
|
|
2322
|
+
case "connecting":
|
|
2323
|
+
return "Connecting\u2026";
|
|
2324
|
+
case "live": {
|
|
2325
|
+
const total = m.total;
|
|
2326
|
+
if (!total) return "Mint";
|
|
2327
|
+
const legs = total.erc20 ? [total.erc20, ...total.native.value > 0n ? [total.native] : []] : [total.native];
|
|
2328
|
+
return `Mint \xB7 ${legs.map((leg) => `${leg.formatted} ${leg.symbol}`).join(" + ")}`.trimEnd();
|
|
2329
|
+
}
|
|
2330
|
+
case "ineligible":
|
|
2331
|
+
return "Not eligible";
|
|
2332
|
+
case "insufficient-funds": {
|
|
2333
|
+
const erc20 = m.total?.erc20;
|
|
2334
|
+
const shortage = erc20 && m.message === `Not enough ${erc20.symbol} to cover the mint.` ? erc20.symbol : m.total?.native.symbol ?? symbol;
|
|
2335
|
+
return `Insufficient ${shortage}`.trimEnd();
|
|
2336
|
+
}
|
|
2337
|
+
case "switching-chain":
|
|
2338
|
+
return "Switching network\u2026";
|
|
2339
|
+
case "approving":
|
|
2340
|
+
return `Approving ${symbol}\u2026`.replace(" \u2026", "\u2026");
|
|
2341
|
+
case "minting":
|
|
2342
|
+
return "Confirm in wallet\u2026";
|
|
2343
|
+
case "confirming":
|
|
2344
|
+
return "Minting\u2026";
|
|
2345
|
+
case "success":
|
|
2346
|
+
return "Minted";
|
|
2347
|
+
case "error":
|
|
2348
|
+
return "Try again";
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
function MintButton(props) {
|
|
2352
|
+
const { className, labels, children, ...options } = props;
|
|
2353
|
+
const m = useMint(options);
|
|
2354
|
+
if (typeof children === "function") return children(m);
|
|
2355
|
+
const label = labels?.[m.state] ?? defaultLabel(m);
|
|
2356
|
+
return createElement(
|
|
2357
|
+
"button",
|
|
2358
|
+
{
|
|
2359
|
+
type: "button",
|
|
2360
|
+
className,
|
|
2361
|
+
"data-state": m.state,
|
|
2362
|
+
...m.reason ? { "data-reason": m.reason } : {},
|
|
2363
|
+
...isAsyncState(m.state) ? { "aria-busy": "true" } : {},
|
|
2364
|
+
disabled: !m.canAct,
|
|
2365
|
+
onClick: m.mint
|
|
2366
|
+
},
|
|
2367
|
+
children ?? label
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// src/index.ts
|
|
2372
|
+
var VERSION = "0.1.0";
|
|
2373
|
+
export {
|
|
2374
|
+
CLICKABLE_STATES,
|
|
2375
|
+
MAX_QUANTITY,
|
|
2376
|
+
MINT_STATES,
|
|
2377
|
+
MintButton,
|
|
2378
|
+
MintError,
|
|
2379
|
+
REASON_CODES,
|
|
2380
|
+
VERSION,
|
|
2381
|
+
isMintError,
|
|
2382
|
+
useMint
|
|
2383
|
+
};
|
|
2384
|
+
//# sourceMappingURL=index.js.map
|