@manifoldxyz/dropkit 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  // src/react/useMint.ts
2
2
  import { useCallback, useEffect, useReducer, useRef, useState } from "react";
3
- import { BaseError as BaseError3, ExecutionRevertedError, publicActions } from "viem";
3
+ import { BaseError as BaseError3, ExecutionRevertedError, publicActions as publicActions2 } from "viem";
4
4
 
5
5
  // src/core/rpc/index.ts
6
6
  import { createPublicClient, http } from "viem";
7
- import { apeChain, base, mainnet, optimism, sepolia, shape } from "viem/chains";
7
+ import { apeChain, base, baseSepolia, mainnet, optimism, sepolia, shape } from "viem/chains";
8
8
 
9
9
  // src/core/errors.ts
10
10
  var UNSUPPORTED_REASONS = [
@@ -22,7 +22,12 @@ var ERROR_REASONS = [
22
22
  "RATE_LIMITED",
23
23
  "RPC_ERROR",
24
24
  "CHAIN_SWITCH_FAILED",
25
- "ALLOWLIST_UNAVAILABLE"
25
+ "ALLOWLIST_UNAVAILABLE",
26
+ // Relay cross-chain mint (spec relay-cross-chain-mint #execute-click).
27
+ "BRIDGE_UNAVAILABLE",
28
+ "BRIDGE_REFUNDED",
29
+ "BRIDGE_FAILED",
30
+ "BRIDGE_TIMEOUT"
26
31
  ];
27
32
  var FUNDS_REASONS = ["INSUFFICIENT_FUNDS"];
28
33
  var REASON_CODES = [
@@ -35,11 +40,14 @@ var MintError = class extends Error {
35
40
  reason;
36
41
  /** INVALID_CONFIG only: dotted path of the bad field, e.g. "mint.args[1]" or "price". */
37
42
  field;
43
+ /** 'relay': the message is Relay's decoded destination revert, shown as-is. */
44
+ domain;
38
45
  constructor(reason, message, opts) {
39
46
  super(message, opts?.cause === void 0 ? void 0 : { cause: opts.cause });
40
47
  this.name = "MintError";
41
48
  this.reason = reason;
42
49
  if (opts?.field !== void 0) this.field = opts.field;
50
+ if (opts?.domain !== void 0) this.domain = opts.domain;
43
51
  }
44
52
  };
45
53
  function isMintError(e) {
@@ -61,8 +69,22 @@ var CHAINS = {
61
69
  [base.id]: base,
62
70
  [optimism.id]: optimism,
63
71
  [shape.id]: shape,
64
- [apeChain.id]: apeChain
72
+ [apeChain.id]: apeChain,
73
+ // Relay pay chain for Sepolia drops only: never a drop chain, no Manifold bridge.
74
+ [baseSepolia.id]: baseSepolia
65
75
  };
76
+ var CHAIN_NAMES = {
77
+ 1: "Ethereum",
78
+ 8453: "Base",
79
+ 10: "Optimism",
80
+ 360: "Shape",
81
+ 33139: "ApeChain",
82
+ 11155111: "Sepolia",
83
+ 84532: "Base Sepolia"
84
+ };
85
+ function chainName(chainId) {
86
+ return CHAIN_NAMES[chainId] ?? CHAINS[chainId]?.name ?? `chain ${String(chainId)}`;
87
+ }
66
88
  var TRANSPORT_TIMEOUT_MS = 8e3;
67
89
  function getChain(chainId) {
68
90
  const chain = CHAINS[chainId];
@@ -133,7 +155,7 @@ function createReadClient(opts) {
133
155
  }
134
156
 
135
157
  // src/core/wallet/index.ts
136
- import { createWalletClient, custom, getAddress } from "viem";
158
+ import { createWalletClient, custom, getAddress, publicActions } from "viem";
137
159
  var CONNECT_WIDGET_VERSION = "6.4.0";
138
160
  var CONNECT_BASE_URL = `https://connect.manifoldxyz.dev/${CONNECT_WIDGET_VERSION}`;
139
161
  var CONNECT_ID = "mint-button";
@@ -300,6 +322,37 @@ function getWalletClient(chainId) {
300
322
  const request = ((args) => provider.request(args));
301
323
  return createWalletClient({ chain: getChain(chainId), transport: custom({ request }) });
302
324
  }
325
+ async function waitForDeposit(chainId, hash, timeoutMs = 12e4) {
326
+ try {
327
+ const receipt = await getWalletClient(chainId).extend(publicActions).waitForTransactionReceipt({ hash, confirmations: 1, timeout: timeoutMs });
328
+ return receipt.status === "reverted" ? "reverted" : "mined";
329
+ } catch {
330
+ return "unknown";
331
+ }
332
+ }
333
+ var WALLET_READ_TIMEOUT = { ms: 1e4 };
334
+ async function getConnectedBalance(address, chainId) {
335
+ const provider = eip1193();
336
+ let timer;
337
+ const timeout = new Promise((_, reject) => {
338
+ timer = setTimeout(() => reject(new MintError("RPC_ERROR", "The wallet did not report a balance in time.")), WALLET_READ_TIMEOUT.ms);
339
+ });
340
+ try {
341
+ return await Promise.race([
342
+ (async () => {
343
+ const current = Number(await provider.request({ method: "eth_chainId" }));
344
+ if (current !== chainId) {
345
+ throw new MintError("CHAIN_SWITCH_FAILED", `The wallet is on chain ${String(current)}, not ${String(chainId)}.`);
346
+ }
347
+ const hex = await provider.request({ method: "eth_getBalance", params: [address, "latest"] });
348
+ return BigInt(hex);
349
+ })(),
350
+ timeout
351
+ ]);
352
+ } finally {
353
+ clearTimeout(timer);
354
+ }
355
+ }
303
356
  var code = (e) => e?.code;
304
357
  async function switchToChain(chainId) {
305
358
  const chain = getChain(chainId);
@@ -331,17 +384,244 @@ async function switchToChain(chainId) {
331
384
  }
332
385
  }
333
386
 
387
+ // src/core/relay/client.ts
388
+ import { formatUnits, getAddress as getAddress2, isHex, keccak256, zeroAddress } from "viem";
389
+ var RELAY_API = { mainnet: "https://api.relay.link", testnet: "https://api.testnets.relay.link" };
390
+ var MAINNET_PAY_CHAINS = [1, 8453, 10, 360, 33139];
391
+ var MAINNET_DROP_CHAINS = [1, 8453, 10, 360];
392
+ var SEPOLIA = 11155111;
393
+ var BASE_SEPOLIA = 84532;
394
+ var isRelayTestnet = (dropChainId) => dropChainId === SEPOLIA;
395
+ function payChainsFor(dropChainId) {
396
+ if (MAINNET_DROP_CHAINS.includes(dropChainId)) return [...MAINNET_PAY_CHAINS];
397
+ if (dropChainId === SEPOLIA) return [SEPOLIA, BASE_SEPOLIA];
398
+ return [];
399
+ }
400
+ var CACHE_MS = 3e4;
401
+ var cache = /* @__PURE__ */ new Map();
402
+ var cacheKey = (a) => [a.testnet, a.originChainId, a.destinationChainId, a.tx.to.toLowerCase(), a.tx.value, keccak256(a.tx.data), a.payer.toLowerCase(), a.recipient.toLowerCase()].join(":");
403
+ var big = (v) => {
404
+ if (typeof v === "string" && /^\d+$/.test(v)) return BigInt(v);
405
+ if (typeof v === "number" && Number.isSafeInteger(v) && v >= 0) return BigInt(v);
406
+ return 0n;
407
+ };
408
+ var obj = (v) => v !== null && typeof v === "object" ? v : {};
409
+ function moneyOf(amount, currency) {
410
+ const c = obj(currency);
411
+ const decimals = typeof c.decimals === "number" ? c.decimals : 18;
412
+ const symbol = typeof c.symbol === "string" ? c.symbol : "ETH";
413
+ const value = big(amount);
414
+ return { value, decimals, symbol, formatted: formatUnits(value, decimals) };
415
+ }
416
+ function revertReason(errorData) {
417
+ if (typeof errorData !== "string") return "";
418
+ const text2 = errorData.replace(/^execution reverted:?\s*/i, "").trim();
419
+ return text2 === "execution reverted" ? "" : text2;
420
+ }
421
+ function parseQuote(body, originChainId) {
422
+ const b = obj(body);
423
+ const steps = Array.isArray(b.steps) ? b.steps.map(obj) : [];
424
+ const unknownStep = () => new MintError("BRIDGE_UNAVAILABLE", "Relay returned a route this button cannot execute.");
425
+ if (steps.length !== 1 || steps[0].kind !== "transaction") throw unknownStep();
426
+ const step2 = steps[0];
427
+ const rawItems = Array.isArray(step2.items) ? step2.items.map(obj) : [];
428
+ if (rawItems.length !== 1) throw unknownStep();
429
+ const items = rawItems.map((item) => {
430
+ const d = obj(item.data);
431
+ const chainId = Number(d.chainId);
432
+ if (chainId !== originChainId || typeof d.to !== "string" || !isHex(d.data)) throw unknownStep();
433
+ return { chainId, to: getAddress2(d.to), data: d.data, value: big(d.value), gas: d.gas === void 0 ? null : big(d.gas) };
434
+ });
435
+ const requestId = isHex(step2.requestId) ? step2.requestId : isHex(b.requestId) ? b.requestId : null;
436
+ if (!requestId) throw unknownStep();
437
+ const details = obj(b.details);
438
+ const currencyIn = obj(details.currencyIn);
439
+ const fees = obj(b.fees);
440
+ const amountIn = moneyOf(currencyIn.amount, currencyIn.currency);
441
+ if (obj(currencyIn.currency).address !== void 0 && obj(currencyIn.currency).address !== zeroAddress) throw unknownStep();
442
+ if (items[0].value !== amountIn.value) throw unknownStep();
443
+ return {
444
+ requestId,
445
+ originChainId,
446
+ items,
447
+ amountIn,
448
+ relayFee: moneyOf(obj(fees.relayer).amount, obj(fees.relayer).currency ?? currencyIn.currency),
449
+ originGas: moneyOf(obj(fees.gas).amount, obj(fees.gas).currency ?? currencyIn.currency),
450
+ quotedAt: Math.floor(Date.now() / 1e3)
451
+ };
452
+ }
453
+ var RELAY_TIMEOUTS = { quoteMs: 1e4, statusMs: 1e4 };
454
+ function raceSignal(p, signal) {
455
+ if (!signal) return p;
456
+ if (signal.aborted) return Promise.reject(new DOMException("Relay quote cancelled", "AbortError"));
457
+ return new Promise((resolve, reject) => {
458
+ const onAbort = () => reject(new DOMException("Relay quote cancelled", "AbortError"));
459
+ signal.addEventListener("abort", onAbort, { once: true });
460
+ p.then(
461
+ (v) => {
462
+ signal.removeEventListener("abort", onAbort);
463
+ resolve(v);
464
+ },
465
+ (e) => {
466
+ signal.removeEventListener("abort", onAbort);
467
+ reject(e instanceof Error ? e : new Error(String(e)));
468
+ }
469
+ );
470
+ });
471
+ }
472
+ async function timedFetch(f, url, init, ms) {
473
+ const controller = new AbortController();
474
+ let t;
475
+ const timeout = new Promise((_, reject) => {
476
+ t = setTimeout(() => {
477
+ controller.abort();
478
+ reject(new DOMException("Relay request timed out", "TimeoutError"));
479
+ }, ms);
480
+ });
481
+ try {
482
+ return await Promise.race([
483
+ (async () => {
484
+ const res = await f(url, { ...init, signal: controller.signal });
485
+ const json = await res.json().catch(() => null);
486
+ return { ok: res.ok, status: res.status, json };
487
+ })(),
488
+ timeout
489
+ ]);
490
+ } finally {
491
+ clearTimeout(t);
492
+ }
493
+ }
494
+ async function requestQuote(a) {
495
+ const f = a.fetch ?? globalThis.fetch;
496
+ const body = {
497
+ user: a.payer,
498
+ recipient: a.recipient,
499
+ refundTo: a.payer,
500
+ // R11: without refundTo, Relay disables automatic refunds.
501
+ originChainId: a.originChainId,
502
+ destinationChainId: a.destinationChainId,
503
+ originCurrency: zeroAddress,
504
+ destinationCurrency: zeroAddress,
505
+ tradeType: "EXACT_OUTPUT",
506
+ amount: a.tx.value.toString(),
507
+ txs: [{ to: a.tx.to, value: a.tx.value.toString(), data: a.tx.data }]
508
+ };
509
+ let res;
510
+ try {
511
+ res = await timedFetch(f, `${a.testnet ? RELAY_API.testnet : RELAY_API.mainnet}/quote`, {
512
+ method: "POST",
513
+ headers: { "Content-Type": "application/json" },
514
+ body: JSON.stringify(body)
515
+ }, RELAY_TIMEOUTS.quoteMs);
516
+ } catch (e) {
517
+ throw new MintError("BRIDGE_UNAVAILABLE", "Could not reach Relay for a cross-chain quote.", { cause: e });
518
+ }
519
+ const json = res.json;
520
+ if (res.ok) return parseQuote(json, a.originChainId);
521
+ const j = obj(json);
522
+ if (j.errorCode === "DESTINATION_TX_FAILED") {
523
+ const reason = revertReason(j.errorData);
524
+ throw new MintError("SIMULATION_REVERTED", reason || "The mint would fail on the drop network.", { cause: json, domain: "relay" });
525
+ }
526
+ if (res.status === 401 || j.errorCode === "UNAUTHORIZED_QUOTE") {
527
+ console.warn("dropkit: Relay rejected the quote (401 UNAUTHORIZED_QUOTE). Relay gates quotes that carry a referrer or need an API key.");
528
+ throw new MintError("BRIDGE_UNAVAILABLE", "Relay refused the cross-chain quote.", { cause: json });
529
+ }
530
+ if (res.status === 429) throw new MintError("RATE_LIMITED", "Relay is rate limiting quotes. Try again in a moment.", { cause: json });
531
+ throw new MintError("BRIDGE_UNAVAILABLE", `Relay could not quote this mint (${String(res.status)}).`, { cause: json });
532
+ }
533
+ function getQuote(a) {
534
+ const k = cacheKey(a);
535
+ const now = Date.now();
536
+ const hit = cache.get(k);
537
+ if (!a.fresh && hit && now - hit.at < CACHE_MS) return raceSignal(hit.quote, a.signal);
538
+ const quote = requestQuote(a);
539
+ cache.set(k, { at: now, quote });
540
+ quote.catch(() => {
541
+ if (cache.get(k)?.quote === quote) cache.delete(k);
542
+ });
543
+ return raceSignal(quote, a.signal);
544
+ }
545
+ var realReason = (v) => typeof v === "string" && v !== "" && v !== "N/A" ? v : null;
546
+ async function getStatus(a) {
547
+ const f = a.fetch ?? globalThis.fetch;
548
+ const res = await raceSignal(timedFetch(f, `${a.testnet ? RELAY_API.testnet : RELAY_API.mainnet}/intents/status/v3?requestId=${a.requestId}`, {}, RELAY_TIMEOUTS.statusMs), a.signal);
549
+ if (!res.ok) throw new Error(`Relay status ${String(res.status)}`);
550
+ const j = obj(res.json);
551
+ return {
552
+ status: typeof j.status === "string" ? j.status : "unknown",
553
+ txHashes: Array.isArray(j.txHashes) ? j.txHashes.filter((h) => isHex(h)) : [],
554
+ failReason: realReason(j.failReason),
555
+ refundFailReason: realReason(j.refundFailReason)
556
+ };
557
+ }
558
+ var KEEP_POLLING = ["waiting", "depositing", "pending", "submitted", "delayed"];
559
+ var TERMINAL = ["success", "refund", "failure"];
560
+
561
+ // src/core/relay/status.ts
562
+ var POLL_FAST_MS = 2e3;
563
+ var POLL_SLOW_MS = 5e3;
564
+ var POLL_FAST_FOR_MS = 6e4;
565
+ var POLL_CAP_MS = 10 * 6e4;
566
+ var defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
567
+ const t = setTimeout(resolve, ms);
568
+ signal?.addEventListener("abort", () => {
569
+ clearTimeout(t);
570
+ reject(new DOMException("Relay polling cancelled", "AbortError"));
571
+ }, { once: true });
572
+ });
573
+ async function pollUntilTerminal(a) {
574
+ const sleep = a.sleep ?? defaultSleep;
575
+ const now = a.now ?? Date.now;
576
+ const start = now();
577
+ let warned = false;
578
+ for (; ; ) {
579
+ a.assertActive?.();
580
+ try {
581
+ const status = await getStatus({ testnet: a.testnet, requestId: a.requestId, ...a.signal ? { signal: a.signal } : {}, ...a.fetch ? { fetch: a.fetch } : {} });
582
+ if (TERMINAL.includes(status.status)) return { kind: "terminal", status };
583
+ if (!KEEP_POLLING.includes(status.status) && !warned) {
584
+ warned = true;
585
+ console.warn(`dropkit: Relay status "${status.status}" for ${a.requestId}; still waiting.`);
586
+ }
587
+ } catch (e) {
588
+ if (e?.name === "AbortError") throw e;
589
+ }
590
+ a.assertActive?.();
591
+ const elapsed = now() - start;
592
+ if (elapsed >= POLL_CAP_MS) return { kind: "timeout" };
593
+ await sleep(elapsed < POLL_FAST_FOR_MS ? POLL_FAST_MS : POLL_SLOW_MS, a.signal);
594
+ }
595
+ }
596
+
597
+ // src/core/relay/outstanding.ts
598
+ var memory = /* @__PURE__ */ new Map();
599
+ var claimed = /* @__PURE__ */ new Set();
600
+ function outstandingKey(config, payer) {
601
+ const target = config.type === "manifold-claim" ? `${config.creatorContract.toLowerCase()}:${config.instanceId}` : config.address.toLowerCase();
602
+ return `${String(config.chainId)}:${target}:${payer.toLowerCase()}`;
603
+ }
604
+ var getOutstanding = (key) => memory.get(key) ?? null;
605
+ var setOutstanding = (key, out) => void memory.set(key, out);
606
+ var clearOutstanding = (key) => void memory.delete(key);
607
+ function claimDeposit(key) {
608
+ if (claimed.has(key) || memory.has(key)) return false;
609
+ claimed.add(key);
610
+ return true;
611
+ }
612
+ var releaseDeposit = (key) => void claimed.delete(key);
613
+
334
614
  // src/core/adapters/manifold-claim/index.ts
335
615
  import {
336
616
  BaseError,
337
617
  ContractFunctionRevertedError,
338
618
  decodeErrorResult,
339
619
  encodeFunctionData,
340
- formatUnits,
341
- getAddress as getAddress2,
620
+ formatUnits as formatUnits2,
621
+ getAddress as getAddress3,
342
622
  isAddress,
343
- isHex,
344
- zeroAddress,
623
+ isHex as isHex2,
624
+ zeroAddress as zeroAddress2,
345
625
  zeroHash
346
626
  } from "viem";
347
627
 
@@ -412,13 +692,13 @@ async function getJson(fetchFn, url, signal, what) {
412
692
  }
413
693
  var isRecord = (v) => typeof v === "object" && v !== null;
414
694
  var scopeCaches = /* @__PURE__ */ new WeakMap();
415
- function memo(cache, key, signal, load) {
416
- const hit = cache.get(key);
695
+ function memo(cache2, key, signal, load) {
696
+ const hit = cache2.get(key);
417
697
  if (hit) return hit;
418
698
  const p = withSignal(load(), signal);
419
- cache.set(key, p);
699
+ cache2.set(key, p);
420
700
  p.catch(() => {
421
- if (cache.get(key) === p) cache.delete(key);
701
+ if (cache2.get(key) === p) cache2.delete(key);
422
702
  });
423
703
  return p;
424
704
  }
@@ -468,8 +748,8 @@ async function fetchMerkleEntries(args) {
468
748
  scoped = { signal: args.signal, instances: /* @__PURE__ */ new Map(), entries: /* @__PURE__ */ new Map() };
469
749
  scopeCaches.set(args.scope, scoped);
470
750
  }
471
- const cache = scoped;
472
- const entries = await memo(cache.entries, key, cache.signal, () => loadEntries(fetchFn, args.api, args.instanceId, args.recipient, cache));
751
+ const cache2 = scoped;
752
+ const entries = await memo(cache2.entries, key, cache2.signal, () => loadEntries(fetchFn, args.api, args.instanceId, args.recipient, cache2));
473
753
  return entries.slice();
474
754
  }
475
755
 
@@ -534,12 +814,12 @@ async function rpc(fn) {
534
814
  function revertData(e) {
535
815
  const hit = e.walk((x) => {
536
816
  const d2 = x.data;
537
- return isHex(d2) && d2.length >= 10 || typeof d2 === "object" && d2 !== null && isHex(d2.data);
817
+ return isHex2(d2) && d2.length >= 10 || typeof d2 === "object" && d2 !== null && isHex2(d2.data);
538
818
  });
539
819
  const d = hit?.data;
540
- if (isHex(d)) return d;
820
+ if (isHex2(d)) return d;
541
821
  const inner = d?.data;
542
- return isHex(inner) ? inner : void 0;
822
+ return isHex2(inner) ? inner : void 0;
543
823
  }
544
824
  function decodeRevert(e) {
545
825
  if (!(e instanceof BaseError)) return null;
@@ -557,7 +837,7 @@ function decodeRevert(e) {
557
837
  const named = e.walk((x) => ["ExecutionRevertedError", "InsufficientFundsError"].includes(x.name));
558
838
  return named instanceof BaseError ? named.shortMessage : null;
559
839
  }
560
- var money = (value, decimals, symbol) => ({ value, decimals, symbol, formatted: formatUnits(value, decimals) });
840
+ var money = (value, decimals, symbol) => ({ value, decimals, symbol, formatted: formatUnits2(value, decimals) });
561
841
  function timestamp(v, field) {
562
842
  if (v === 0n) return null;
563
843
  if (v > BigInt(Number.MAX_SAFE_INTEGER)) throw new MintError("RPC_ERROR", `Claim ${field} does not fit a safe integer`);
@@ -574,14 +854,14 @@ function parseConfig(config) {
574
854
  throw new MintError("INVALID_CONFIG", "instanceId must be a non-negative integer string.", { field: "instanceId" });
575
855
  }
576
856
  return {
577
- creator: getAddress2(config.creatorContract),
857
+ creator: getAddress3(config.creatorContract),
578
858
  instanceId: BigInt(config.instanceId),
579
- extension: config.extension === void 0 ? void 0 : getAddress2(config.extension)
859
+ extension: config.extension === void 0 ? void 0 : getAddress3(config.extension)
580
860
  };
581
861
  }
582
862
  var notFound = (config) => new MintError("CLAIM_NOT_FOUND", `No Manifold claim ${config.instanceId} found on ${config.creatorContract}.`);
583
863
  async function readClaim(client, entry, creator, instanceId, config) {
584
- const address = getAddress2(entry.address);
864
+ const address = getAddress3(entry.address);
585
865
  const args = [creator, instanceId];
586
866
  try {
587
867
  switch (entry.abi) {
@@ -620,7 +900,7 @@ function base2(c) {
620
900
  };
621
901
  }
622
902
  async function hasCode(client, address) {
623
- const code2 = await rpc(() => client.getCode({ address: getAddress2(address) }));
903
+ const code2 = await rpc(() => client.getCode({ address: getAddress3(address) }));
624
904
  return code2 !== void 0 && code2 !== "0x";
625
905
  }
626
906
  var unknownExtension = (address, why) => new MintError("UNKNOWN_EXTENSION", `Extension ${address} ${why}.`);
@@ -664,21 +944,21 @@ async function resolveExtension(ctx, config) {
664
944
  }
665
945
  async function loadDrop(ctx, config) {
666
946
  const { entry, claim } = await resolveExtension(ctx, config);
667
- if (claim.signingAddress !== null && claim.signingAddress !== zeroAddress) {
947
+ if (claim.signingAddress !== null && claim.signingAddress !== zeroAddress2) {
668
948
  throw new MintError("SIGNATURE_CLAIM", "This claim mints with a signature (redemption code), which this button does not support.");
669
949
  }
670
950
  const { client } = ctx;
671
- const extension = getAddress2(entry.address);
951
+ const extension = getAddress3(entry.address);
672
952
  const fee = await rpc(
673
953
  () => client.readContract({ address: extension, abi: EXTENSION_COMMON_ABI, functionName: isAllowlist(claim) ? "MINT_FEE_MERKLE" : "MINT_FEE" })
674
954
  );
675
955
  const native = client.chain?.nativeCurrency ?? { symbol: "ETH", decimals: 18 };
676
956
  let unitPrice;
677
957
  let paymentToken = null;
678
- if (claim.erc20 === zeroAddress) {
958
+ if (claim.erc20 === zeroAddress2) {
679
959
  unitPrice = money(claim.cost, native.decimals, native.symbol);
680
960
  } else {
681
- paymentToken = getAddress2(claim.erc20);
961
+ paymentToken = getAddress3(claim.erc20);
682
962
  const token = paymentToken;
683
963
  const [symbol, decimals] = await rpc(
684
964
  () => Promise.all([
@@ -709,9 +989,9 @@ async function loadWallet(ctx, config, args) {
709
989
  const { client } = ctx;
710
990
  const resolved = dropResolved.get(args.drop) ?? (await loadDrop(ctx, config)).resolved;
711
991
  const { entry, claim } = resolved;
712
- const extension = getAddress2(entry.address);
713
- const payer = getAddress2(args.payer);
714
- const recipient = getAddress2(args.recipient);
992
+ const extension = getAddress3(entry.address);
993
+ const payer = getAddress3(args.payer);
994
+ const recipient = getAddress3(args.recipient);
715
995
  let walletMinted = null;
716
996
  let walletMax = null;
717
997
  let allowlistRemaining = null;
@@ -747,7 +1027,7 @@ async function loadWallet(ctx, config, args) {
747
1027
  maxQuantity = Number(left);
748
1028
  if (left === 0n) ineligible = { reason: "WALLET_LIMIT", message: `This wallet has already minted its limit of ${walletMax}.` };
749
1029
  }
750
- const token = claim.erc20 === zeroAddress ? null : getAddress2(claim.erc20);
1030
+ const token = claim.erc20 === zeroAddress2 ? null : getAddress3(claim.erc20);
751
1031
  const [native, erc20, gasPrice] = await rpc(
752
1032
  () => Promise.all([
753
1033
  client.getBalance({ address: payer }),
@@ -775,8 +1055,8 @@ async function buildPlan(ctx, attempt, wallet) {
775
1055
  throw new MintError("INVALID_CONFIG", `quantity must be an integer from 1 to ${MAX_MINT_COUNT}.`, { field: "quantity" });
776
1056
  }
777
1057
  const { creator, instanceId } = parseConfig(config);
778
- const recipient = getAddress2(attempt.recipient);
779
- const payer = getAddress2(attempt.payer);
1058
+ const recipient = getAddress3(attempt.recipient);
1059
+ const payer = getAddress3(attempt.payer);
780
1060
  let data = wallet.data;
781
1061
  if (!isWalletData(data) || !sameClaim(data.config, config) || data.recipient !== recipient) {
782
1062
  const drop = (await loadDrop(ctx, config)).drop;
@@ -784,7 +1064,7 @@ async function buildPlan(ctx, attempt, wallet) {
784
1064
  }
785
1065
  if (!isWalletData(data)) throw new MintError("RPC_ERROR", "Wallet read produced no plan data.");
786
1066
  const { entry, claim, fee } = data.resolved;
787
- const extension = getAddress2(entry.address);
1067
+ const extension = getAddress3(entry.address);
788
1068
  let mintIndices = [];
789
1069
  let merkleProofs = [];
790
1070
  if (isAllowlist(claim)) {
@@ -796,7 +1076,7 @@ async function buildPlan(ctx, attempt, wallet) {
796
1076
  merkleProofs = picked.map((p) => p.proof);
797
1077
  }
798
1078
  const qty = BigInt(quantity);
799
- const erc20 = claim.erc20 === zeroAddress ? null : getAddress2(claim.erc20);
1079
+ const erc20 = claim.erc20 === zeroAddress2 ? null : getAddress3(claim.erc20);
800
1080
  const cost = claim.cost * qty;
801
1081
  const value = fee * qty + (erc20 ? 0n : cost);
802
1082
  let approve = null;
@@ -825,13 +1105,18 @@ async function buildPlan(ctx, attempt, wallet) {
825
1105
  tokenContract: creator
826
1106
  };
827
1107
  }
1108
+ var RELAY_VERIFIED = ["V6", "V7"];
828
1109
  var manifoldClaimAdapter = {
1110
+ supportsRelay: (drop) => {
1111
+ const resolved = dropResolved.get(drop);
1112
+ return resolved !== void 0 && RELAY_VERIFIED.includes(resolved.entry.generation) && drop.paymentToken === null;
1113
+ },
829
1114
  readDrop: async (ctx, config) => (await loadDrop(ctx, config)).drop,
830
1115
  readWallet: loadWallet,
831
1116
  buildPlan,
832
1117
  simulate: async (ctx, attempt, plan) => {
833
1118
  try {
834
- await ctx.client.call({ account: getAddress2(attempt.payer), to: plan.mint.to, data: plan.mint.data, value: plan.mint.value });
1119
+ await ctx.client.call({ account: getAddress3(attempt.payer), to: plan.mint.to, data: plan.mint.data, value: plan.mint.value });
835
1120
  } catch (e) {
836
1121
  const text2 = decodeRevert(e);
837
1122
  if (text2 === null) throw toRpcError(e);
@@ -848,15 +1133,15 @@ import {
848
1133
  InsufficientFundsError,
849
1134
  decodeErrorResult as decodeErrorResult2,
850
1135
  encodeFunctionData as encodeFunctionData2,
851
- formatUnits as formatUnits3,
852
- getAddress as getAddress3,
1136
+ formatUnits as formatUnits4,
1137
+ getAddress as getAddress4,
853
1138
  isAddress as isAddress2,
854
1139
  parseAbi as parseAbi2,
855
1140
  parseUnits
856
1141
  } from "viem";
857
1142
 
858
1143
  // src/core/machine.ts
859
- import { formatUnits as formatUnits2 } from "viem";
1144
+ import { formatUnits as formatUnits3 } from "viem";
860
1145
  var MINT_STATES = [
861
1146
  "loading",
862
1147
  "unsupported",
@@ -893,9 +1178,15 @@ function initialState(opts) {
893
1178
  attempt: null,
894
1179
  result: null,
895
1180
  pendingRefresh: false,
896
- now: 0
1181
+ now: 0,
1182
+ crossChain: false,
1183
+ routePending: false,
1184
+ route: null,
1185
+ short: null
897
1186
  };
898
1187
  }
1188
+ var NO_ROUTE = { crossChain: false, routePending: false, route: null, short: null };
1189
+ var PICK_ANOTHER = "Pick another network to pay from.";
899
1190
  function toQuantity(q, max) {
900
1191
  if (Number.isNaN(q)) return 1;
901
1192
  return Math.max(1, Math.min(Math.floor(q), max));
@@ -920,11 +1211,17 @@ function derive(s, unsupported) {
920
1211
  if (walletRead.ineligible !== null) {
921
1212
  return { name: "ineligible", reason: walletRead.ineligible.reason, message: walletRead.ineligible.message };
922
1213
  }
1214
+ if (s.routePending) return named("loading");
1215
+ if (s.route?.kind === "relay") return named("live");
1216
+ if (s.short !== null) {
1217
+ return { name: "insufficient-funds", reason: "INSUFFICIENT_FUNDS", message: `Not enough ${s.short.symbol} on ${s.short.name}. ${PICK_ANOTHER}` };
1218
+ }
923
1219
  const total = selectTotal(s);
924
1220
  if (total !== null) {
925
1221
  const { native, erc20 } = total;
926
1222
  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.` };
1223
+ const more = s.crossChain ? ` ${PICK_ANOTHER}` : "";
1224
+ return { name: "insufficient-funds", reason: "INSUFFICIENT_FUNDS", message: `Not enough ${native.symbol} to cover the mint.${more}` };
928
1225
  }
929
1226
  if (erc20 !== null && walletRead.balances.erc20 !== null && erc20.value > walletRead.balances.erc20) {
930
1227
  return { name: "insufficient-funds", reason: "INSUFFICIENT_FUNDS", message: `Not enough ${erc20.symbol} to cover the mint.` };
@@ -953,7 +1250,7 @@ function reducer(state, event) {
953
1250
  const next2 = { ...state, readGen: event.gen };
954
1251
  return inFlight ? { ...next2, pendingRefresh: true } : settle(next2);
955
1252
  }
956
- const next = { ...state, readGen: event.gen, drop: null, walletRead: null, blocked: null, walletGen: state.walletGen + 1 };
1253
+ const next = { ...state, ...NO_ROUTE, readGen: event.gen, drop: null, walletRead: null, blocked: null, walletGen: state.walletGen + 1 };
957
1254
  return inFlight ? { ...next, pendingRefresh: true } : settle(next, false);
958
1255
  }
959
1256
  case "DROP_LOADED": {
@@ -971,16 +1268,22 @@ function reducer(state, event) {
971
1268
  case "READ_FAILED":
972
1269
  return event.gen !== state.readGen ? state : failed(event.reason, event.message);
973
1270
  case "ACCOUNT_CHANGED": {
974
- const next = { ...state, account: event.account, walletRead: null, blocked: null, walletGen: state.walletGen + 1 };
1271
+ const next = { ...state, ...NO_ROUTE, account: event.account, walletRead: null, blocked: null, walletGen: state.walletGen + 1 };
975
1272
  if (inFlight) return { ...next, pendingRefresh: true };
976
1273
  if (state.name === "connecting") return event.account === null ? next : settle({ ...next, name: "loading" }, false);
977
1274
  return settle(next);
978
1275
  }
979
1276
  case "WALLET_LOADED": {
980
1277
  if (event.gen !== state.walletGen) return state;
981
- const next = { ...state, walletRead: event.read, blocked: null };
1278
+ const route = event.routePending ? { routePending: state.route === null && state.short === null, route: state.route, short: state.short } : { routePending: false, route: null, short: null };
1279
+ const next = { ...state, ...route, crossChain: event.crossChain ?? false, walletRead: event.read, blocked: null };
982
1280
  return inFlight ? { ...next, pendingRefresh: true } : settle(clampQuantity(next));
983
1281
  }
1282
+ case "ROUTE_LOADED": {
1283
+ if (event.gen !== state.walletGen) return state;
1284
+ const next = { ...state, routePending: false, route: event.route, short: event.short };
1285
+ return inFlight ? { ...next, pendingRefresh: true } : settle(next);
1286
+ }
984
1287
  case "WALLET_FAILED":
985
1288
  return event.gen !== state.walletGen ? state : failed(event.reason, event.message);
986
1289
  case "CONNECT_STARTED":
@@ -994,7 +1297,9 @@ function reducer(state, event) {
994
1297
  return inFlight ? { ...next, pendingRefresh: true } : settle(next);
995
1298
  }
996
1299
  case "ATTEMPT_STARTED":
997
- return state.name === "live" && state.attempt === null ? { ...state, attempt: event.attempt } : state;
1300
+ if (state.attempt !== null) return state;
1301
+ if (event.resume === true) return IN_FLIGHT_STATES.includes(state.name) ? state : { ...state, attempt: event.attempt };
1302
+ return state.name === "live" ? { ...state, attempt: event.attempt } : state;
998
1303
  case "SWITCHING_CHAIN":
999
1304
  return step(state, event.attemptId, "switching-chain");
1000
1305
  case "APPROVING":
@@ -1036,6 +1341,7 @@ function reducer(state, event) {
1036
1341
  const next = {
1037
1342
  ...state,
1038
1343
  readGen: event.gen,
1344
+ ...NO_ROUTE,
1039
1345
  walletRead: null,
1040
1346
  blocked: null,
1041
1347
  walletGen: state.walletGen + 1,
@@ -1081,7 +1387,7 @@ function selectTotal(state) {
1081
1387
  return { native: money2(priceTotal + fee.value * q, price), erc20: null };
1082
1388
  }
1083
1389
  function money2(value, like) {
1084
- return { value, decimals: like.decimals, symbol: like.symbol, formatted: formatUnits2(value, like.decimals) };
1390
+ return { value, decimals: like.decimals, symbol: like.symbol, formatted: formatUnits3(value, like.decimals) };
1085
1391
  }
1086
1392
  function canAct(state) {
1087
1393
  return state.attempt === null && CLICKABLE_STATES.includes(state.name);
@@ -1318,7 +1624,7 @@ function varFor(token, vars) {
1318
1624
  const key = token.slice(1, -1);
1319
1625
  const v = vars[key];
1320
1626
  if (v === void 0) throw new MintError("INVALID_CONFIG", `placeholder ${token} has no value here`);
1321
- return typeof v === "string" ? getAddress3(v) : v;
1627
+ return typeof v === "string" ? getAddress4(v) : v;
1322
1628
  }
1323
1629
  function fill(t, p, vars) {
1324
1630
  const arr = ARRAY_TYPE.exec(p.type);
@@ -1407,7 +1713,7 @@ function nativeMeta(ctx) {
1407
1713
  const n = ctx.client.chain?.nativeCurrency;
1408
1714
  return { decimals: n?.decimals ?? 18, symbol: n?.symbol ?? "ETH" };
1409
1715
  }
1410
- var erc20Of = (config) => config.price.currency === "native" ? null : getAddress3(config.price.currency.address);
1716
+ var erc20Of = (config) => config.price.currency === "native" ? null : getAddress4(config.price.currency.address);
1411
1717
  async function currencyMeta(ctx, config) {
1412
1718
  const token = erc20Of(config);
1413
1719
  if (!token) return { token, ...nativeMeta(ctx) };
@@ -1439,7 +1745,7 @@ var money3 = (value, decimals, symbol) => ({
1439
1745
  value,
1440
1746
  decimals,
1441
1747
  symbol,
1442
- formatted: formatUnits3(value, decimals)
1748
+ formatted: formatUnits4(value, decimals)
1443
1749
  });
1444
1750
  function revertData2(e) {
1445
1751
  for (let cur = e, depth = 0; cur && depth < 12; depth++) {
@@ -1552,7 +1858,7 @@ async function readWallet(ctx, config, { payer, recipient, quantity, drop }) {
1552
1858
  if (remaining === 0n) {
1553
1859
  ineligible = { reason: "WALLET_LIMIT", message: `This wallet has already minted its limit of ${walletMax}.` };
1554
1860
  } else if (drop.paymentToken === null && native >= totalPrice) {
1555
- const call = { payer, to: getAddress3(config.address), data: encodeMint(config, vars), value: mintValue(config, null, totalPrice) };
1861
+ const call = { payer, to: getAddress4(config.address), data: encodeMint(config, vars), value: mintValue(config, null, totalPrice) };
1556
1862
  try {
1557
1863
  await simulateMint(ctx, config, call);
1558
1864
  gas = await io(ctx, () => ctx.client.estimateGas({ account: payer, to: call.to, data: call.data, value: call.value })).catch((e) => {
@@ -1583,7 +1889,7 @@ async function buildPlan2(ctx, attempt, wallet) {
1583
1889
  const qty = BigInt(attempt.quantity);
1584
1890
  const unitPrice = await resolvePrice(ctx, config);
1585
1891
  const totalPrice = totalFor(config.price.per ?? "unit", unitPrice, qty);
1586
- const to = getAddress3(config.address);
1892
+ const to = getAddress4(config.address);
1587
1893
  const token = erc20Of(config);
1588
1894
  const mint = {
1589
1895
  to,
@@ -1605,7 +1911,7 @@ async function buildPlan2(ctx, attempt, wallet) {
1605
1911
  };
1606
1912
  }
1607
1913
  }
1608
- return { approve, mint, tokenContract: getAddress3(attempt.tokenContract) };
1914
+ return { approve, mint, tokenContract: getAddress4(attempt.tokenContract) };
1609
1915
  }
1610
1916
  async function simulate(ctx, attempt, plan) {
1611
1917
  throwIfAborted(ctx);
@@ -1614,28 +1920,28 @@ async function simulate(ctx, attempt, plan) {
1614
1920
  var contractAdapter = { readDrop, readWallet, buildPlan: buildPlan2, simulate };
1615
1921
 
1616
1922
  // src/core/adapters/receipt.ts
1617
- import { decodeAbiParameters, getAddress as getAddress4, zeroAddress as zeroAddress2 } from "viem";
1923
+ import { decodeAbiParameters, getAddress as getAddress5, zeroAddress as zeroAddress3 } from "viem";
1618
1924
  var TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
1619
1925
  var TRANSFER_SINGLE_TOPIC = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62";
1620
1926
  var TRANSFER_BATCH_TOPIC = "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb";
1621
- var topicAddress = (t) => t && t.length === 66 ? getAddress4(`0x${t.slice(26)}`) : null;
1927
+ var topicAddress = (t) => t && t.length === 66 ? getAddress5(`0x${t.slice(26)}`) : null;
1622
1928
  function parseMintReceipt(logs, { tokenContract, recipient }) {
1623
- const contract = getAddress4(tokenContract);
1624
- const to = getAddress4(recipient);
1929
+ const contract = getAddress5(tokenContract);
1930
+ const to = getAddress5(recipient);
1625
1931
  const out = /* @__PURE__ */ new Map();
1626
1932
  const add = (id, amount) => out.set(id, (out.get(id) ?? 0n) + amount);
1627
1933
  for (const log of logs) {
1628
- if (getAddress4(log.address) !== contract) continue;
1934
+ if (getAddress5(log.address) !== contract) continue;
1629
1935
  const [t0, t1, t2, t3] = log.topics;
1630
1936
  if (t0 === TRANSFER_TOPIC && log.topics.length === 4) {
1631
- if (topicAddress(t1) !== zeroAddress2 || topicAddress(t2) !== to || !t3) continue;
1937
+ if (topicAddress(t1) !== zeroAddress3 || topicAddress(t2) !== to || !t3) continue;
1632
1938
  add(BigInt(t3), 1n);
1633
1939
  } else if (t0 === TRANSFER_SINGLE_TOPIC && log.topics.length === 4) {
1634
- if (topicAddress(t2) !== zeroAddress2 || topicAddress(t3) !== to) continue;
1940
+ if (topicAddress(t2) !== zeroAddress3 || topicAddress(t3) !== to) continue;
1635
1941
  const [id, value] = decodeAbiParameters([{ type: "uint256" }, { type: "uint256" }], log.data);
1636
1942
  add(id, value);
1637
1943
  } else if (t0 === TRANSFER_BATCH_TOPIC && log.topics.length === 4) {
1638
- if (topicAddress(t2) !== zeroAddress2 || topicAddress(t3) !== to) continue;
1944
+ if (topicAddress(t2) !== zeroAddress3 || topicAddress(t3) !== to) continue;
1639
1945
  const [ids, values] = decodeAbiParameters([{ type: "uint256[]" }, { type: "uint256[]" }], log.data);
1640
1946
  if (ids.length !== values.length) continue;
1641
1947
  ids.forEach((id, i) => add(id, values[i]));
@@ -1683,6 +1989,7 @@ function friendlyErrorMessage(error) {
1683
1989
  if (isMintError(error)) {
1684
1990
  if (error.reason === "INSUFFICIENT_FUNDS") return fundsMessage(collect(error.cause)) ?? error.message;
1685
1991
  if (error.reason !== "RPC_ERROR" && error.reason !== "SIMULATION_REVERTED") return error.message;
1992
+ if (error.reason === "SIMULATION_REVERTED" && error.domain === "relay") return error.message;
1686
1993
  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
1994
  }
1688
1995
  const errors = collect(error);
@@ -1744,6 +2051,25 @@ function adapterFor(config) {
1744
2051
  return config.type === "manifold-claim" ? manifoldClaimAdapter : contractAdapter;
1745
2052
  }
1746
2053
  var bridgeEnabled = (config) => config.type === "manifold-claim";
2054
+ function payChainsOf(config, option) {
2055
+ if (config.type !== "manifold-claim" || option === false) return [];
2056
+ const all = payChainsFor(config.chainId);
2057
+ const narrowed = typeof option === "object" && option.chains ? all.filter((id) => id === config.chainId || option.chains.includes(id)) : all;
2058
+ return narrowed.length > 1 ? narrowed : [];
2059
+ }
2060
+ function routeChainOf(payChains, accountChainId, payChainId) {
2061
+ if (payChainId !== void 0 && payChains.includes(payChainId)) return payChainId;
2062
+ return accountChainId;
2063
+ }
2064
+ var ROUTE_DEBOUNCE_MS = 500;
2065
+ var symbolOf = (chainId) => {
2066
+ try {
2067
+ return getChain(chainId).nativeCurrency.symbol;
2068
+ } catch {
2069
+ return "ETH";
2070
+ }
2071
+ };
2072
+ var shortOf = (chainId) => ({ chainId, symbol: symbolOf(chainId), name: chainName(chainId) });
1747
2073
  function tokenContractOf(config) {
1748
2074
  return config.type === "manifold-claim" ? config.creatorContract : config.tokenContract ?? config.address;
1749
2075
  }
@@ -1801,6 +2127,8 @@ function useMint(options) {
1801
2127
  const widgetFailRef = useRef(null);
1802
2128
  const widgetLoadRef = useRef(null);
1803
2129
  const pushAccountRef = useRef(null);
2130
+ const autoResumedRef = useRef(/* @__PURE__ */ new Set());
2131
+ const [crossChainInfo, setCrossChainInfo] = useState(null);
1804
2132
  useEffect(() => {
1805
2133
  stateRef.current = machine;
1806
2134
  propsRef.current = options;
@@ -1816,6 +2144,7 @@ function useMint(options) {
1816
2144
  if (!handler || !mounted.current) return;
1817
2145
  const cfg = attemptRef.current?.config ?? propsRef.current.config;
1818
2146
  const s = stateRef.current;
2147
+ const originChainId = attemptRef.current?.originChainId ?? (s.route?.kind === "relay" ? s.route.originChainId : s.short?.chainId ?? cfg.chainId);
1819
2148
  const payload = {
1820
2149
  type,
1821
2150
  state: s.name,
@@ -1823,6 +2152,8 @@ function useMint(options) {
1823
2152
  chainId: cfg.chainId,
1824
2153
  quantity: s.quantity,
1825
2154
  config: { type: cfg.type, chainId: cfg.chainId, address: eventAddressOf(cfg) },
2155
+ route: originChainId === cfg.chainId ? "direct" : "relay",
2156
+ originChainId,
1826
2157
  ...over
1827
2158
  };
1828
2159
  try {
@@ -1942,7 +2273,7 @@ function useMint(options) {
1942
2273
  if (cancelled) throw new MintError("USER_REJECTED", "The transaction was cancelled or replaced with a different transaction.");
1943
2274
  if (await walletFor().getChainId() !== attempt.chainId) throw e;
1944
2275
  assertActive();
1945
- receipt2 = await walletFor().extend(publicActions).waitForTransactionReceipt(params);
2276
+ receipt2 = await walletFor().extend(publicActions2).waitForTransactionReceipt(params);
1946
2277
  }
1947
2278
  assertActive();
1948
2279
  if (cancelled) throw new MintError("USER_REJECTED", "The transaction was cancelled or replaced with a different transaction.");
@@ -2022,6 +2353,156 @@ function useMint(options) {
2022
2353
  },
2023
2354
  [clientFor, ctxFor, dispatch, emit]
2024
2355
  );
2356
+ const finishRelay = useCallback(
2357
+ async (attempt, out, life) => {
2358
+ const cfg = attempt.config;
2359
+ const client = clientFor(life, cfg);
2360
+ const assertActive = () => {
2361
+ if (!mounted.current || life.controller.signal.aborted || lifeRef.current !== life || attemptRef.current !== attempt) {
2362
+ throw new DOMException("Mint attempt cancelled", "AbortError");
2363
+ }
2364
+ };
2365
+ const outcome = await pollUntilTerminal({ testnet: out.testnet, requestId: out.requestId, signal: life.controller.signal, assertActive });
2366
+ assertActive();
2367
+ const where = chainName(out.originChainId);
2368
+ if (outcome.kind === "timeout") {
2369
+ throw new MintError("BRIDGE_TIMEOUT", "The bridge is still processing. Your mint may still complete.");
2370
+ }
2371
+ const key = outstandingKey(attempt.config, attempt.payer);
2372
+ const { status } = outcome;
2373
+ if (status.status !== "success") clearOutstanding(key);
2374
+ if (status.status === "refund") {
2375
+ throw new MintError("BRIDGE_REFUNDED", `Relay could not complete the mint and is refunding your payment. Relay request ${out.requestId}.`);
2376
+ }
2377
+ if (status.status === "failure") {
2378
+ const why = status.failReason ? ` (${status.failReason})` : "";
2379
+ const noRefund = status.refundFailReason ? ` No automatic refund was sent. Contact support with Relay request ${out.requestId}.` : "";
2380
+ throw new MintError("BRIDGE_FAILED", `Relay could not complete the mint paid on ${where}${why}.${noRefund}`);
2381
+ }
2382
+ const fillHash = status.txHashes[0] ?? null;
2383
+ let tokens = [];
2384
+ if (fillHash) {
2385
+ try {
2386
+ const receipt = await client.waitForTransactionReceipt({ hash: fillHash, confirmations: 1, timeout: 6e4 });
2387
+ if (receiptOk(receipt.status)) {
2388
+ tokens = parseMintReceipt(receipt.logs ?? [], { tokenContract: attempt.tokenContract, recipient: attempt.recipient });
2389
+ }
2390
+ } catch {
2391
+ }
2392
+ assertActive();
2393
+ }
2394
+ const txHash = fillHash ?? out.originTxHash;
2395
+ const result = {
2396
+ txHash,
2397
+ explorerUrl: explorerUrlFor(fillHash ? cfg.chainId : out.originChainId, txHash),
2398
+ tokens,
2399
+ relay: { requestId: out.requestId, originTxHash: out.originTxHash, originChainId: out.originChainId }
2400
+ };
2401
+ clearOutstanding(key);
2402
+ dispatch({ type: "ATTEMPT_SUCCEEDED", attemptId: attempt.attemptId, result });
2403
+ emit("success", { state: "success", reason: null, txHash, quantity: attempt.quantity, chainId: cfg.chainId });
2404
+ },
2405
+ [clientFor, dispatch, emit]
2406
+ );
2407
+ const executeRelayAttempt = useCallback(
2408
+ async (attempt, walletRead, life, account2) => {
2409
+ const cfg = attempt.config;
2410
+ const adapter = adapterFor(cfg);
2411
+ const ctx = ctxFor(life, cfg);
2412
+ const testnet = isRelayTestnet(cfg.chainId);
2413
+ const origin = attempt.originChainId;
2414
+ const assertPayer = () => {
2415
+ if (!mounted.current || life.controller.signal.aborted || lifeRef.current !== life || attemptRef.current !== attempt) {
2416
+ throw new DOMException("Mint attempt cancelled", "AbortError");
2417
+ }
2418
+ const live = getAccount();
2419
+ if (!live || live.address.toLowerCase() !== attempt.payer.toLowerCase()) {
2420
+ throw new MintError("USER_REJECTED", "The connected wallet changed before the transaction was sent.");
2421
+ }
2422
+ };
2423
+ assertPayer();
2424
+ if (account2.chainId !== origin) {
2425
+ dispatch({ type: "SWITCHING_CHAIN", attemptId: attempt.attemptId });
2426
+ await switchToChain(origin);
2427
+ }
2428
+ assertPayer();
2429
+ const plan = await adapter.buildPlan(ctx, attempt, walletRead);
2430
+ if (plan.approve) throw new MintError("BRIDGE_UNAVAILABLE", "Cross-chain mint supports native-priced drops only.");
2431
+ assertPayer();
2432
+ const quote = await getQuote({
2433
+ testnet,
2434
+ payer: attempt.payer,
2435
+ recipient: attempt.recipient,
2436
+ originChainId: origin,
2437
+ destinationChainId: cfg.chainId,
2438
+ tx: plan.mint,
2439
+ fresh: true,
2440
+ signal: life.controller.signal
2441
+ });
2442
+ assertPayer();
2443
+ const balance = await getConnectedBalance(attempt.payer, origin);
2444
+ if (balance < quote.amountIn.value + quote.originGas.value) {
2445
+ const s = shortOf(origin);
2446
+ throw new MintError("INSUFFICIENT_FUNDS", `Not enough ${s.symbol} on ${s.name}. Pick another network to pay from.`);
2447
+ }
2448
+ assertPayer();
2449
+ const item = quote.items[0];
2450
+ const key = outstandingKey(cfg, attempt.payer);
2451
+ const wallet2 = getWalletClient(origin);
2452
+ assertPayer();
2453
+ if (!claimDeposit(key)) {
2454
+ throw new MintError("BRIDGE_UNAVAILABLE", "A cross-chain payment for this drop is already in progress. Try again once it finishes.");
2455
+ }
2456
+ let out;
2457
+ try {
2458
+ dispatch({ type: "MINT_PROMPTED", attemptId: attempt.attemptId });
2459
+ const send = wallet2;
2460
+ const originTxHash2 = await send.sendTransaction({
2461
+ account: attempt.payer,
2462
+ to: item.to,
2463
+ data: item.data,
2464
+ value: item.value,
2465
+ ...item.gas !== null && item.gas > 0n ? { gas: item.gas } : {},
2466
+ chain: getChain(origin)
2467
+ });
2468
+ out = { requestId: quote.requestId, testnet, originChainId: origin, originTxHash: originTxHash2, attempt };
2469
+ setOutstanding(key, out);
2470
+ } finally {
2471
+ releaseDeposit(key);
2472
+ }
2473
+ const { originTxHash } = out;
2474
+ autoResumedRef.current.add(key);
2475
+ dispatch({ type: "TX_SUBMITTED", attemptId: attempt.attemptId });
2476
+ emit("tx-submitted", { state: "confirming", reason: null, txHash: originTxHash, quantity: attempt.quantity, chainId: cfg.chainId });
2477
+ const deposit = await waitForDeposit(origin, originTxHash);
2478
+ if (deposit === "reverted") {
2479
+ clearOutstanding(key);
2480
+ throw new MintError("TX_REVERTED", `The payment transaction reverted (${originTxHash}).`);
2481
+ }
2482
+ await finishRelay(attempt, out, life);
2483
+ },
2484
+ [ctxFor, dispatch, emit, finishRelay]
2485
+ );
2486
+ const resumeOutstanding = useCallback(
2487
+ async (out, life) => {
2488
+ const attempt = Object.freeze({ ...out.attempt, attemptId: ++attemptIdRef.current });
2489
+ attemptRef.current = attempt;
2490
+ dispatch({ type: "ATTEMPT_STARTED", attempt, resume: true });
2491
+ dispatch({ type: "TX_SUBMITTED", attemptId: attempt.attemptId });
2492
+ emit("attempt", { state: "confirming", reason: null, quantity: attempt.quantity, chainId: attempt.chainId });
2493
+ try {
2494
+ await finishRelay(attempt, out, life);
2495
+ } catch (e) {
2496
+ if (!mounted.current || life.controller.signal.aborted || lifeRef.current !== life) return;
2497
+ const { reason, message } = toReason(e);
2498
+ dispatch({ type: "ATTEMPT_FAILED", attemptId: attempt.attemptId, reason, message });
2499
+ emit("error", { quantity: attempt.quantity, chainId: attempt.chainId });
2500
+ } finally {
2501
+ attemptRef.current = null;
2502
+ }
2503
+ },
2504
+ [dispatch, emit, finishRelay]
2505
+ );
2025
2506
  const runMint = useCallback(async () => {
2026
2507
  if (!mounted.current) return;
2027
2508
  if (attemptRef.current !== null) return;
@@ -2058,6 +2539,12 @@ function useMint(options) {
2058
2539
  }
2059
2540
  return;
2060
2541
  }
2542
+ const payer = s.account?.address;
2543
+ const out = payer ? getOutstanding(outstandingKey(propsRef.current.config, payer)) : null;
2544
+ if (out && lifeRef.current) {
2545
+ await resumeOutstanding(out, lifeRef.current);
2546
+ return;
2547
+ }
2061
2548
  if (s.name === "success" || s.name === "error") {
2062
2549
  const life2 = lifeRef.current;
2063
2550
  const gen = ++genRef.current;
@@ -2073,6 +2560,7 @@ function useMint(options) {
2073
2560
  const life = lifeRef.current;
2074
2561
  if (!account2 || !walletRead || !life) return;
2075
2562
  const cfg = propsRef.current.config;
2563
+ const relayRoute = s.route?.kind === "relay" ? s.route : null;
2076
2564
  const attempt = Object.freeze({
2077
2565
  attemptId: ++attemptIdRef.current,
2078
2566
  config: structuredClone(cfg),
@@ -2080,13 +2568,15 @@ function useMint(options) {
2080
2568
  recipient: propsRef.current.recipient ?? account2.address,
2081
2569
  quantity: s.quantity,
2082
2570
  chainId: cfg.chainId,
2083
- tokenContract: tokenContractOf(cfg)
2571
+ tokenContract: tokenContractOf(cfg),
2572
+ originChainId: relayRoute ? relayRoute.originChainId : cfg.chainId
2084
2573
  });
2085
2574
  attemptRef.current = attempt;
2086
2575
  dispatch({ type: "ATTEMPT_STARTED", attempt });
2087
2576
  emit("attempt", { state: "live", reason: null, quantity: attempt.quantity, chainId: attempt.chainId });
2088
2577
  try {
2089
- await executeAttempt(attempt, walletRead, life, account2);
2578
+ if (relayRoute) await executeRelayAttempt(attempt, walletRead, life, account2);
2579
+ else await executeAttempt(attempt, walletRead, life, account2);
2090
2580
  } catch (e) {
2091
2581
  if (!mounted.current || life.controller.signal.aborted || lifeRef.current !== life) return;
2092
2582
  const { reason, message } = toReason(e);
@@ -2095,7 +2585,7 @@ function useMint(options) {
2095
2585
  } finally {
2096
2586
  attemptRef.current = null;
2097
2587
  }
2098
- }, [dispatch, emit, readDrop2, executeAttempt, refreshNow]);
2588
+ }, [dispatch, emit, readDrop2, executeAttempt, executeRelayAttempt, resumeOutstanding, refreshNow]);
2099
2589
  const mint = useCallback(() => {
2100
2590
  void runMint();
2101
2591
  }, [runMint]);
@@ -2130,9 +2620,10 @@ function useMint(options) {
2130
2620
  pushAccountRef.current?.(getAccount());
2131
2621
  }, [recipient, machine.attempt, isBusy]);
2132
2622
  const projectId = connect?.walletConnectProjectId;
2623
+ const widgetChains = [config.chainId, ...payChainsOf(config, options.crossChain).filter((id) => id !== config.chainId)].join(",");
2133
2624
  useEffect(() => {
2134
2625
  let alive = true;
2135
- const loading = loadConnectWidget({ chainIds: [config.chainId], ...projectId ? { walletConnectProjectId: projectId } : {} });
2626
+ const loading = loadConnectWidget({ chainIds: widgetChains.split(",").map(Number), ...projectId ? { walletConnectProjectId: projectId } : {} });
2136
2627
  widgetLoadRef.current = loading;
2137
2628
  loading.then(
2138
2629
  () => {
@@ -2147,7 +2638,7 @@ function useMint(options) {
2147
2638
  return () => {
2148
2639
  alive = false;
2149
2640
  };
2150
- }, [config.chainId, projectId, widgetRetry]);
2641
+ }, [widgetChains, projectId, widgetRetry]);
2151
2642
  useEffect(() => {
2152
2643
  if (!mounted.current) return;
2153
2644
  if (isBusy()) return;
@@ -2164,6 +2655,22 @@ function useMint(options) {
2164
2655
  void refreshNow();
2165
2656
  }
2166
2657
  });
2658
+ const resumeKey = machine.account ? outstandingKey(config, machine.account.address) : null;
2659
+ useEffect(() => {
2660
+ if (!resumeKey || autoResumedRef.current.has(resumeKey) || attemptRef.current !== null || isBusy()) return;
2661
+ const out = getOutstanding(resumeKey);
2662
+ const life = lifeRef.current;
2663
+ if (!out || !life) return;
2664
+ autoResumedRef.current.add(resumeKey);
2665
+ void resumeOutstanding(out, life);
2666
+ }, [resumeKey, machine.attempt, isBusy, resumeOutstanding]);
2667
+ const routeOptionsKey = `${String(options.payChainId ?? "")}|${JSON.stringify(options.crossChain ?? null)}`;
2668
+ const previousRouteOptions = useRef(routeOptionsKey);
2669
+ useEffect(() => {
2670
+ if (previousRouteOptions.current === routeOptionsKey || isBusy()) return;
2671
+ previousRouteOptions.current = routeOptionsKey;
2672
+ if (stateRef.current.account) dispatch({ type: "ACCOUNT_CHANGED", account: stateRef.current.account });
2673
+ }, [routeOptionsKey, machine.attempt, dispatch, isBusy]);
2167
2674
  const previousQuantity = useRef(machine.quantity);
2168
2675
  useEffect(() => {
2169
2676
  if (previousQuantity.current === machine.quantity || isBusy()) return;
@@ -2196,7 +2703,61 @@ function useMint(options) {
2196
2703
  });
2197
2704
  if (!alive || life.controller.signal.aborted || lifeRef.current !== life || life.gen !== readGen) return;
2198
2705
  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 } });
2706
+ const fullRead = { ...read, ineligible };
2707
+ const payChains = payChainsOf(cfg, propsRef.current.crossChain);
2708
+ const crossChain = payChains.length > 0 && ineligible === null && (adapter.supportsRelay?.(drop) ?? false);
2709
+ const routeChain = routeChainOf(payChains, account.chainId, propsRef.current.payChainId);
2710
+ const routePending = crossChain && routeChain !== cfg.chainId && payChains.includes(routeChain);
2711
+ dispatch({ type: "WALLET_LOADED", gen: walletGen, read: fullRead, crossChain, routePending });
2712
+ if (!crossChain) {
2713
+ setCrossChainInfo(null);
2714
+ return;
2715
+ }
2716
+ const recipientAddr = propsRef.current.recipient ?? account.address;
2717
+ const draft = {
2718
+ attemptId: 0,
2719
+ config: cfg,
2720
+ payer: account.address,
2721
+ recipient: recipientAddr,
2722
+ quantity: stateRef.current.quantity,
2723
+ chainId: cfg.chainId,
2724
+ tokenContract: tokenContractOf(cfg),
2725
+ originChainId: routeChain
2726
+ };
2727
+ const testnet = isRelayTestnet(cfg.chainId);
2728
+ let tx;
2729
+ try {
2730
+ tx = (await adapter.buildPlan(ctxFor(life, cfg), draft, fullRead)).mint;
2731
+ } catch {
2732
+ if (!alive) return;
2733
+ setCrossChainInfo(null);
2734
+ if (routePending) dispatch({ type: "ROUTE_LOADED", gen: walletGen, route: { kind: "direct", chainId: cfg.chainId }, short: null });
2735
+ return;
2736
+ }
2737
+ if (!alive || life.controller.signal.aborted || lifeRef.current !== life) return;
2738
+ setCrossChainInfo((prev) => {
2739
+ const same = prev && prev.info.dropChainId === cfg.chainId && prev.info.testnet === testnet && prev.info.tx.to === tx.to && prev.info.tx.data === tx.data && prev.info.tx.value === tx.value && prev.info.payer === account.address && prev.info.recipient === recipientAddr && prev.info.payChains.join() === payChains.join();
2740
+ return { gen: walletGen, info: same ? prev.info : { dropChainId: cfg.chainId, payChains, tx, payer: account.address, recipient: recipientAddr, testnet } };
2741
+ });
2742
+ if (!routePending) return;
2743
+ await new Promise((r) => setTimeout(r, ROUTE_DEBOUNCE_MS));
2744
+ if (!alive || life.controller.signal.aborted || lifeRef.current !== life) return;
2745
+ let route = { kind: "direct", chainId: cfg.chainId };
2746
+ let short = null;
2747
+ try {
2748
+ const quote = await getQuote({ testnet, payer: account.address, recipient: recipientAddr, originChainId: routeChain, destinationChainId: cfg.chainId, tx, signal: life.controller.signal });
2749
+ const relay = { kind: "relay", originChainId: routeChain, amountIn: quote.amountIn, relayFee: quote.relayFee, originGas: quote.originGas, quotedAt: quote.quotedAt };
2750
+ if (routeChain !== account.chainId) {
2751
+ route = relay;
2752
+ } else {
2753
+ const balance = await getConnectedBalance(account.address, routeChain);
2754
+ if (balance >= quote.amountIn.value + quote.originGas.value) route = relay;
2755
+ else short = shortOf(routeChain);
2756
+ }
2757
+ } catch {
2758
+ }
2759
+ if (!alive || life.controller.signal.aborted || lifeRef.current !== life) return;
2760
+ dispatch({ type: "ROUTE_LOADED", gen: walletGen, route: short ? null : route, short });
2200
2761
  } catch (e) {
2201
2762
  if (!alive || life.controller.signal.aborted || lifeRef.current !== life || life.gen !== readGen) return;
2202
2763
  dispatch({ type: "WALLET_FAILED", gen: walletGen, ...errorReasonOf(e) });
@@ -2296,12 +2857,137 @@ function useMint(options) {
2296
2857
  result: machine.result,
2297
2858
  refresh,
2298
2859
  // 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
2860
+ canAct: canAct({ ...machine, name: effective.name }) && machine.attempt === null,
2861
+ // Short on the wallet chain is neither route: nothing can be paid until the collector picks another chain.
2862
+ route: machine.route ?? (machine.walletRead && machine.drop && !machine.routePending && machine.short === null ? { kind: "direct", chainId: machine.drop.chainId } : null),
2863
+ crossChain: machine.crossChain && machine.account && crossChainInfo?.gen === machine.walletGen ? crossChainInfo.info : null
2300
2864
  };
2301
2865
  }
2302
2866
 
2867
+ // src/react/PayWithPicker.ts
2868
+ import { createElement, useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
2869
+ import { formatUnits as formatUnits5 } from "viem";
2870
+ var PICKER_REFRESH_MS = 2e4;
2871
+ var PICKER_DEBOUNCE_MS = 500;
2872
+ var RELAY_REASONS = /* @__PURE__ */ new Set(["BRIDGE_UNAVAILABLE", "BRIDGE_TIMEOUT", "BRIDGE_REFUNDED", "BRIDGE_FAILED"]);
2873
+ var money4 = (value, chainId) => {
2874
+ const c = getChain(chainId).nativeCurrency;
2875
+ return { value, decimals: c.decimals, symbol: c.symbol, formatted: formatUnits5(value, c.decimals) };
2876
+ };
2877
+ function usePayOptions(mint, onSelect) {
2878
+ const info = mint.crossChain;
2879
+ const current = mint.wallet?.chainId ?? null;
2880
+ const [quotes, setQuotes] = useState2({});
2881
+ const [balance, setBalance] = useState2(null);
2882
+ const [tick, setTick] = useState2(0);
2883
+ useEffect2(() => {
2884
+ const t = setInterval(() => setTick((n) => n + 1), PICKER_REFRESH_MS);
2885
+ return () => clearInterval(t);
2886
+ }, []);
2887
+ const key = info ? `${info.tx.to}:${info.tx.value}:${info.tx.data}:${info.payer}:${info.recipient}:${info.payChains.join()}` : "";
2888
+ const [quotedKey, setQuotedKey] = useState2(key);
2889
+ if (quotedKey !== key) {
2890
+ setQuotedKey(key);
2891
+ setQuotes({});
2892
+ }
2893
+ useEffect2(() => {
2894
+ if (!info) return;
2895
+ const controller = new AbortController();
2896
+ const timer = setTimeout(() => {
2897
+ for (const chainId of info.payChains) {
2898
+ if (chainId === info.dropChainId) continue;
2899
+ getQuote({ testnet: info.testnet, payer: info.payer, recipient: info.recipient, originChainId: chainId, destinationChainId: info.dropChainId, tx: info.tx, signal: controller.signal }).then(
2900
+ (q) => {
2901
+ if (!controller.signal.aborted) setQuotes((prev) => ({ ...prev, [chainId]: { relayFee: q.relayFee, amountIn: q.amountIn } }));
2902
+ },
2903
+ (e) => {
2904
+ if (controller.signal.aborted || e?.reason === "RATE_LIMITED") return;
2905
+ setQuotes((prev) => ({ ...prev, [chainId]: "failed" }));
2906
+ }
2907
+ );
2908
+ }
2909
+ }, PICKER_DEBOUNCE_MS);
2910
+ return () => {
2911
+ clearTimeout(timer);
2912
+ controller.abort();
2913
+ };
2914
+ }, [key, tick]);
2915
+ useEffect2(() => {
2916
+ if (!info || current === null || !mint.wallet) return;
2917
+ let alive = true;
2918
+ getConnectedBalance(mint.wallet.address, current).then(
2919
+ (value) => {
2920
+ if (alive) setBalance({ chainId: current, value });
2921
+ },
2922
+ () => {
2923
+ if (alive) setBalance(null);
2924
+ }
2925
+ );
2926
+ return () => {
2927
+ alive = false;
2928
+ };
2929
+ }, [current, mint.wallet?.address, key, tick]);
2930
+ const select = useCallback2(
2931
+ (chainId) => {
2932
+ switchToChain(chainId).then(
2933
+ () => onSelect?.(chainId, null),
2934
+ (e) => onSelect?.(chainId, e)
2935
+ );
2936
+ },
2937
+ [onSelect]
2938
+ );
2939
+ if (!info) return [];
2940
+ return info.payChains.filter((chainId) => chainId === info.dropChainId || quotes[chainId] !== "failed").map((chainId) => {
2941
+ const q = quotes[chainId];
2942
+ const isDropChain = chainId === info.dropChainId;
2943
+ const quoted = q && q !== "failed" ? q : null;
2944
+ return {
2945
+ chainId,
2946
+ name: chainName(chainId),
2947
+ selected: chainId === current,
2948
+ isDropChain,
2949
+ relayFee: isDropChain ? null : quoted?.relayFee ?? null,
2950
+ amountIn: isDropChain ? null : quoted?.amountIn ?? null,
2951
+ balance: balance && balance.chainId === chainId && chainId === current ? money4(balance.value, chainId) : null,
2952
+ loading: !isDropChain && q === void 0,
2953
+ select: () => select(chainId)
2954
+ };
2955
+ });
2956
+ }
2957
+ function defaultRow(row) {
2958
+ const fee = row.isDropChain ? "None" : row.loading ? "\u2026" : row.relayFee ? `${row.relayFee.formatted} ${row.relayFee.symbol}` : "--";
2959
+ const balance = row.balance ? ` \xB7 ${row.balance.formatted} ${row.balance.symbol}` : "";
2960
+ return `${row.name}${balance} \xB7 fee ${fee}`;
2961
+ }
2962
+ function PayWithPicker(props) {
2963
+ const rows = usePayOptions(props.mint, props.onSelect);
2964
+ if (rows.length === 0) return null;
2965
+ const open = props.mint.state === "insufficient-funds" || props.mint.reason !== null && RELAY_REASONS.has(props.mint.reason);
2966
+ return createElement(
2967
+ "div",
2968
+ { className: props.className, role: "group", "aria-label": "Pay with", "data-open": open ? "true" : "false" },
2969
+ rows.map(
2970
+ (row) => createElement(
2971
+ "button",
2972
+ {
2973
+ key: row.chainId,
2974
+ type: "button",
2975
+ "data-chain": String(row.chainId),
2976
+ "data-selected": row.selected ? "true" : "false",
2977
+ "data-loading": row.loading ? "true" : "false",
2978
+ "aria-pressed": row.selected,
2979
+ // Only an idle button may change chains: a click in progress already chose its pay chain.
2980
+ disabled: row.selected || !(props.mint.canAct || props.mint.state === "insufficient-funds" || props.mint.state === "loading"),
2981
+ onClick: row.select
2982
+ },
2983
+ props.renderRow ? props.renderRow(row) : defaultRow(row)
2984
+ )
2985
+ )
2986
+ );
2987
+ }
2988
+
2303
2989
  // src/react/MintButton.ts
2304
- import { createElement } from "react";
2990
+ import { createElement as createElement2 } from "react";
2305
2991
  function defaultLabel(m) {
2306
2992
  const symbol = m.total?.erc20?.symbol ?? m.total?.native.symbol ?? m.drop?.unitPrice.symbol ?? "";
2307
2993
  switch (m.state) {
@@ -2322,6 +3008,10 @@ function defaultLabel(m) {
2322
3008
  case "connecting":
2323
3009
  return "Connecting\u2026";
2324
3010
  case "live": {
3011
+ const route = m.route;
3012
+ if (route?.kind === "relay") {
3013
+ return `Mint \xB7 ${route.amountIn.formatted} ${route.amountIn.symbol} on ${chainName(route.originChainId)}`;
3014
+ }
2325
3015
  const total = m.total;
2326
3016
  if (!total) return "Mint";
2327
3017
  const legs = total.erc20 ? [total.erc20, ...total.native.value > 0n ? [total.native] : []] : [total.native];
@@ -2353,7 +3043,7 @@ function MintButton(props) {
2353
3043
  const m = useMint(options);
2354
3044
  if (typeof children === "function") return children(m);
2355
3045
  const label = labels?.[m.state] ?? defaultLabel(m);
2356
- return createElement(
3046
+ return createElement2(
2357
3047
  "button",
2358
3048
  {
2359
3049
  type: "button",
@@ -2369,16 +3059,18 @@ function MintButton(props) {
2369
3059
  }
2370
3060
 
2371
3061
  // src/index.ts
2372
- var VERSION = "0.1.0";
3062
+ var VERSION = "0.2.0";
2373
3063
  export {
2374
3064
  CLICKABLE_STATES,
2375
3065
  MAX_QUANTITY,
2376
3066
  MINT_STATES,
2377
3067
  MintButton,
2378
3068
  MintError,
3069
+ PayWithPicker,
2379
3070
  REASON_CODES,
2380
3071
  VERSION,
2381
3072
  isMintError,
2382
- useMint
3073
+ useMint,
3074
+ usePayOptions
2383
3075
  };
2384
3076
  //# sourceMappingURL=index.js.map