{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dogecoin-payment-button",
  "title": "Dogecoin Payment Button",
  "author": "Simon Betton",
  "description": "A Button that opens a Send me Dogecoin dialog with BIP44 address discovery, optional mempool payment detection, QR code, and copy support. Prefer resolving the account xpub on the server.",
  "dependencies": [
    "@scure/bip32",
    "@noble/hashes",
    "@scure/base",
    "uqr",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dialog"
  ],
  "files": [
    {
      "path": "registry/default/dogecoin-payment-button/dogecoin-payment-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\n\nimport { isDogecoinP2pkhAddress } from \"./dogecoin\";\nimport { PaymentDialogBody } from \"./payment-button-parts\";\nimport type { DogecoinPaymentButtonProps } from \"./payment-button-types\";\nimport {\n  useMempoolWatch,\n  usePaymentResolution,\n} from \"./use-payment-button-state\";\nimport { watchStatusMessage } from \"./watch-status\";\n\nexport type {\n  DogecoinPaymentButtonProps,\n  DogecoinPaymentResolveFn,\n  MempoolWatchConfig,\n} from \"./payment-button-types\";\n\nconst countModes = ({\n  address,\n  resolveAddress,\n  xpub,\n}: DogecoinPaymentButtonProps): number =>\n  [address, resolveAddress, xpub].filter(Boolean).length;\n\nexport const DogecoinPaymentButton = (props: DogecoinPaymentButtonProps) => {\n  if (countModes(props) !== 1) {\n    throw new Error(\n      \"DogecoinPaymentButton requires exactly one of `address`, `resolveAddress`, or `xpub`.\"\n    );\n  }\n\n  if (props.address && !isDogecoinP2pkhAddress(props.address)) {\n    throw new Error(\n      \"DogecoinPaymentButton received an invalid Dogecoin address.\"\n    );\n  }\n\n  const [open, setOpen] = React.useState(false);\n  const {\n    address: staticAddress,\n    addressIndexStep,\n    amount,\n    buttonLabel,\n    checker,\n    children,\n    className,\n    maxAttempts,\n    mempoolWatch,\n    resolveAddress,\n    xpub,\n  } = props;\n\n  const resolution = usePaymentResolution({\n    address: staticAddress,\n    addressIndexStep,\n    amount,\n    checker,\n    maxAttempts,\n    open,\n    resolveAddress,\n    xpub,\n  });\n\n  const watch = useMempoolWatch({\n    amount,\n    endpoint: mempoolWatch?.endpoint,\n    fetchImpl: mempoolWatch?.fetchImpl,\n    onPaymentAppeared: props.onPaymentAppeared,\n    open,\n    paymentAddress: resolution.payment?.address,\n    resolutionStatus: resolution.status,\n  });\n\n  const watchMessage = watchStatusMessage(watch.status, watch.errorMessage);\n\n  const handleRetryOpen = () => {\n    setOpen(false);\n    queueMicrotask(() => {\n      setOpen(true);\n    });\n  };\n\n  const handleRetryWatch = () => {\n    watch.retryWatch();\n  };\n\n  return (\n    <Dialog onOpenChange={setOpen} open={open}>\n      <Button\n        aria-expanded={open}\n        aria-haspopup=\"dialog\"\n        className={cn(className)}\n        onClick={() => {\n          setOpen(true);\n        }}\n        type=\"button\"\n      >\n        {children ?? buttonLabel ?? \"Send me Dogecoin\"}\n      </Button>\n      <DialogContent className=\"sm:max-w-md\">\n        <DialogHeader>\n          <DialogTitle>Send me Dogecoin</DialogTitle>\n          <DialogDescription>\n            Scan the QR code or copy the address below to send DOGE.\n          </DialogDescription>\n        </DialogHeader>\n\n        <PaymentDialogBody\n          amount={amount}\n          appearedPayload={watch.appearedPayload}\n          errorMessage={resolution.errorMessage}\n          mempoolWatchEnabled={Boolean(mempoolWatch)}\n          onRetryOpen={handleRetryOpen}\n          onRetryWatch={handleRetryWatch}\n          payment={resolution.payment}\n          status={resolution.status}\n          watchMessage={watchMessage}\n          watchStatus={watch.status}\n        />\n      </DialogContent>\n    </Dialog>\n  );\n};\n",
      "type": "registry:component",
      "target": "components/dogecoin-payment-button/dogecoin-payment-button.tsx"
    },
    {
      "path": "registry/default/dogecoin-payment-button/dogecoin.ts",
      "content": "import { ripemd160 } from \"@noble/hashes/legacy.js\";\nimport { sha256 } from \"@noble/hashes/sha2.js\";\nimport { base58check } from \"@scure/base\";\nimport { HDKey } from \"@scure/bip32\";\n\nconst b58c = base58check(sha256);\n\n/** Dogecoin Core BIP32 version bytes (`dgpv` / `dgub`). */\nexport const DOGECOIN_BIP32_VERSIONS = {\n  private: 0x02_fa_c3_98,\n  public: 0x02_fa_ca_fd,\n} as const;\n\n/** Bitcoin BIP32 version bytes accepted for account-public-key interchange. */\nexport const BITCOIN_BIP32_VERSIONS = {\n  private: 0x04_88_ad_e4,\n  public: 0x04_88_b2_1e,\n} as const;\n\nexport const DOGECOIN_P2PKH_VERSION = 0x1e;\nexport const ACCOUNT_XPUB_DEPTH = 3;\nexport const EXTERNAL_CHAIN = 0;\n\nexport class DogecoinKeyError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"DogecoinKeyError\";\n  }\n}\n\nconst detectPublicVersions = (extendedKey: string) => {\n  if (extendedKey.startsWith(\"dgub\")) {\n    return DOGECOIN_BIP32_VERSIONS;\n  }\n\n  if (extendedKey.startsWith(\"xpub\")) {\n    return BITCOIN_BIP32_VERSIONS;\n  }\n\n  if (\n    extendedKey.startsWith(\"dgpv\") ||\n    extendedKey.startsWith(\"xprv\") ||\n    extendedKey.startsWith(\"tprv\") ||\n    extendedKey.startsWith(\"tpub\")\n  ) {\n    throw new DogecoinKeyError(\n      \"Private or testnet extended keys are not accepted. Provide an account-level dgub or xpub.\"\n    );\n  }\n\n  throw new DogecoinKeyError(\n    \"Unsupported extended key encoding. Provide an account-level dgub or xpub.\"\n  );\n};\n\n/**\n * Parse a Dogecoin BIP44 account-level public key (`m/44'/3'/0'`).\n * Accepts Dogecoin-native `dgub` and Bitcoin-compatible `xpub` encodings.\n */\nexport const parseAccountXpub = (extendedKey: string): HDKey => {\n  const trimmed = extendedKey.trim();\n  const versions = detectPublicVersions(trimmed);\n\n  let key: HDKey;\n  try {\n    key = HDKey.fromExtendedKey(trimmed, versions);\n  } catch {\n    throw new DogecoinKeyError(\"Invalid extended public key encoding.\");\n  }\n\n  if (key.privateKey) {\n    throw new DogecoinKeyError(\"Private extended keys are not accepted.\");\n  }\n\n  if (key.depth !== ACCOUNT_XPUB_DEPTH) {\n    const depthHint =\n      key.depth === ACCOUNT_XPUB_DEPTH + 1\n        ? \" This looks like a receive-chain key at m/44'/3'/0'/0 — export the account key at m/44'/3'/0' instead.\"\n        : \"\";\n    throw new DogecoinKeyError(\n      `Expected an account-level key at depth ${ACCOUNT_XPUB_DEPTH} (m/44'/3'/0'), got depth ${key.depth}.${depthHint}`\n    );\n  }\n\n  return key;\n};\n\nexport const publicKeyToP2pkhAddress = (publicKey: Uint8Array): string => {\n  const hash = ripemd160(sha256(publicKey));\n  const payload = new Uint8Array(21);\n  payload[0] = DOGECOIN_P2PKH_VERSION;\n  payload.set(hash, 1);\n  return b58c.encode(payload);\n};\n\n/**\n * Derive a mainnet Dogecoin P2PKH payment address for the external chain.\n * Relative path from the account xpub: `m/0/<addressIndex>`.\n */\nexport const derivePaymentAddress = (\n  accountXpub: string,\n  addressIndex: number\n): string => {\n  if (!Number.isInteger(addressIndex) || addressIndex < 0) {\n    throw new DogecoinKeyError(\"Address index must be a non-negative integer.\");\n  }\n\n  const account = parseAccountXpub(accountXpub);\n  const child = account.derive(`m/${EXTERNAL_CHAIN}/${addressIndex}`);\n\n  if (!child.publicKey) {\n    throw new DogecoinKeyError(\n      \"Failed to derive a public key for the address.\"\n    );\n  }\n\n  return publicKeyToP2pkhAddress(child.publicKey);\n};\n\nexport const isDogecoinP2pkhAddress = (address: string): boolean => {\n  try {\n    const decoded = b58c.decode(address);\n    return decoded.length === 21 && decoded[0] === DOGECOIN_P2PKH_VERSION;\n  } catch {\n    return false;\n  }\n};\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/dogecoin.ts"
    },
    {
      "path": "registry/default/dogecoin-payment-button/address-selection.ts",
      "content": "export const DEFAULT_ADDRESS_INDEX_STEP = 19;\nexport const DEFAULT_MAX_ATTEMPTS = 20;\n\nexport type AddressUsageChecker = (\n  address: string,\n  signal?: AbortSignal\n) => Promise<boolean>;\n\nexport type SelectionMode =\n  | \"verified\"\n  | \"transient-fallback\"\n  | \"exhausted\"\n  | \"unchecked\";\n\nexport interface SelectionState {\n  startIndex: number;\n  uncheckedMode: boolean;\n}\n\nexport interface AddressSelectionResult {\n  address: string;\n  index: number;\n  mode: SelectionMode;\n  nextStartIndex: number;\n  uncheckedMode: boolean;\n}\n\nexport interface AddressSelectionOptions {\n  addressIndexStep?: number;\n  checker: AddressUsageChecker;\n  deriveAddress: (index: number) => string;\n  maxAttempts?: number;\n  signal?: AbortSignal;\n  startIndex?: number;\n  uncheckedMode?: boolean;\n}\n\nconst assertNotAborted = (signal?: AbortSignal): void => {\n  if (signal?.aborted) {\n    throw new DOMException(\"Address selection aborted.\", \"AbortError\");\n  }\n};\n\n/**\n * Select a payment address using the stepped discovery sequence.\n *\n * From `startIndex`, candidates are `start, start+step, …` for up to\n * `maxAttempts` checks. An unused address is reused on later opens until it\n * becomes used. Provider errors fall back to the next unchecked index for the\n * current opening and retry discovery from that index next time. After\n * `maxAttempts` used results, the final known-used candidate is returned and\n * the instance enters permanent unchecked mode (increment-only, no checks).\n */\nexport const selectPaymentAddress = async (\n  options: AddressSelectionOptions\n): Promise<AddressSelectionResult> => {\n  const addressIndexStep =\n    options.addressIndexStep ?? DEFAULT_ADDRESS_INDEX_STEP;\n  const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n  const startIndex = options.startIndex ?? 0;\n  const uncheckedMode = options.uncheckedMode ?? false;\n\n  if (!Number.isInteger(addressIndexStep) || addressIndexStep < 1) {\n    throw new Error(\"addressIndexStep must be a positive integer.\");\n  }\n\n  if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n    throw new Error(\"maxAttempts must be a positive integer.\");\n  }\n\n  if (!Number.isInteger(startIndex) || startIndex < 0) {\n    throw new Error(\"startIndex must be a non-negative integer.\");\n  }\n\n  assertNotAborted(options.signal);\n\n  if (uncheckedMode) {\n    const address = options.deriveAddress(startIndex);\n    return {\n      address,\n      index: startIndex,\n      mode: \"unchecked\",\n      nextStartIndex: startIndex + addressIndexStep,\n      uncheckedMode: true,\n    };\n  }\n\n  let lastCheckedIndex = startIndex;\n  let lastCheckedAddress = options.deriveAddress(startIndex);\n\n  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n    assertNotAborted(options.signal);\n\n    const index = startIndex + attempt * addressIndexStep;\n    const address =\n      attempt === 0 ? lastCheckedAddress : options.deriveAddress(index);\n    lastCheckedIndex = index;\n    lastCheckedAddress = address;\n\n    let used: boolean;\n    try {\n      used = await options.checker(address, options.signal);\n    } catch (error) {\n      if (error instanceof DOMException && error.name === \"AbortError\") {\n        throw error;\n      }\n\n      const fallbackIndex = index + addressIndexStep;\n      return {\n        address: options.deriveAddress(fallbackIndex),\n        index: fallbackIndex,\n        mode: \"transient-fallback\",\n        nextStartIndex: fallbackIndex,\n        uncheckedMode: false,\n      };\n    }\n\n    assertNotAborted(options.signal);\n\n    if (!used) {\n      return {\n        address,\n        index,\n        mode: \"verified\",\n        nextStartIndex: index,\n        uncheckedMode: false,\n      };\n    }\n  }\n\n  return {\n    address: lastCheckedAddress,\n    index: lastCheckedIndex,\n    mode: \"exhausted\",\n    nextStartIndex: lastCheckedIndex + addressIndexStep,\n    uncheckedMode: true,\n  };\n};\n\nexport const createInitialSelectionState = (): SelectionState => ({\n  startIndex: 0,\n  uncheckedMode: false,\n});\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/address-selection.ts"
    },
    {
      "path": "registry/default/dogecoin-payment-button/blockcypher.ts",
      "content": "import type { AddressUsageChecker } from \"./address-selection\";\n\nexport const BLOCKCYPHER_DOGE_ADDRESS_URL =\n  \"https://api.blockcypher.com/v1/doge/main/addrs\";\n\n/** Stay under BlockCypher's free-tier 3 req/sec limit. */\nexport const DEFAULT_REQUEST_INTERVAL_MS = 350;\n\nexport interface BlockCypherCheckerOptions {\n  baseUrl?: string;\n  fetchImpl?: typeof fetch;\n  requestIntervalMs?: number;\n  token?: string;\n}\n\ninterface BlockCypherAddressResponse {\n  final_n_tx?: number;\n}\n\ninterface TimeoutRef {\n  current?: ReturnType<typeof setTimeout>;\n}\n\nlet lastRequestAt = 0;\n\nconst sleep = (ms: number, signal?: AbortSignal): Promise<void> =>\n  // oxlint-disable-next-line promise/avoid-new -- Abortable timers require coordinating resolve and reject callbacks.\n  new Promise((resolve, reject) => {\n    if (signal?.aborted) {\n      reject(new DOMException(\"Address check aborted.\", \"AbortError\"));\n      return;\n    }\n\n    const timeoutRef: TimeoutRef = {};\n\n    const onAbort = () => {\n      if (timeoutRef.current !== undefined) {\n        clearTimeout(timeoutRef.current);\n      }\n      reject(new DOMException(\"Address check aborted.\", \"AbortError\"));\n    };\n\n    timeoutRef.current = setTimeout(() => {\n      signal?.removeEventListener(\"abort\", onAbort);\n      resolve();\n    }, ms);\n\n    signal?.addEventListener(\"abort\", onAbort, { once: true });\n  });\n\nconst paceRequests = async (\n  intervalMs: number,\n  signal?: AbortSignal\n): Promise<void> => {\n  const elapsed = Date.now() - lastRequestAt;\n  if (elapsed < intervalMs) {\n    await sleep(intervalMs - elapsed, signal);\n  }\n  lastRequestAt = Date.now();\n};\n\n/**\n * Default Address Usage Checker backed by BlockCypher's Dogecoin address API.\n * An address is used when `final_n_tx > 0` (confirmed or mempool activity).\n */\nexport const createBlockCypherAddressUsageChecker = (\n  options: BlockCypherCheckerOptions = {}\n): AddressUsageChecker => {\n  const fetchImpl = options.fetchImpl ?? fetch;\n  const intervalMs = options.requestIntervalMs ?? DEFAULT_REQUEST_INTERVAL_MS;\n  const baseUrl = options.baseUrl ?? BLOCKCYPHER_DOGE_ADDRESS_URL;\n\n  return async (address, signal) => {\n    await paceRequests(intervalMs, signal);\n\n    const url = new URL(`${baseUrl}/${address}`);\n    if (options.token) {\n      url.searchParams.set(\"token\", options.token);\n    }\n\n    const response = await fetchImpl(url, {\n      headers: { Accept: \"application/json\" },\n      method: \"GET\",\n      signal,\n    });\n\n    if (!response.ok) {\n      throw new Error(\n        `BlockCypher address lookup failed with status ${response.status}.`\n      );\n    }\n\n    const body = (await response.json()) as BlockCypherAddressResponse;\n\n    if (typeof body.final_n_tx !== \"number\") {\n      throw new TypeError(\"BlockCypher response is missing final_n_tx.\");\n    }\n\n    return body.final_n_tx > 0;\n  };\n};\n\nexport const defaultAddressUsageChecker =\n  createBlockCypherAddressUsageChecker();\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/blockcypher.ts"
    },
    {
      "path": "registry/default/dogecoin-payment-button/mempool-watch.ts",
      "content": "import { isDogecoinP2pkhAddress } from \"./dogecoin\";\nimport { normalizeDogecoinAmount } from \"./payment-uri\";\n\nexport const DOGE_BASE_UNITS_PER_COIN = 100_000_000;\n\nexport const MEMPOOL_WATCH_APPEARED_EVENT = \"mempool.watch.appeared\" as const;\nexport const MEMPOOL_WATCH_TIMEOUT_EVENT = \"mempool.watch.timeout\" as const;\n\nexport type MempoolWatchSource = \"catchup\" | \"live\";\n\nexport interface MempoolWatchOutput {\n  valueBase: string;\n  vout: number;\n}\n\nexport interface MempoolWatchAppearedPayload {\n  address: string;\n  detectedAt: string;\n  outputs: MempoolWatchOutput[];\n  source: MempoolWatchSource;\n  txid: string;\n}\n\nexport interface MempoolWatchTimeoutPayload {\n  address: string;\n  expiresAt: string;\n}\n\nexport type MempoolWatchStatus =\n  | \"idle\"\n  | \"watching\"\n  | \"appeared\"\n  | \"timeout\"\n  | \"busy\"\n  | \"error\";\n\nexport class MempoolWatchError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"MempoolWatchError\";\n  }\n}\n\nexport interface WatchMempoolOptions {\n  address: string;\n  endpoint: string;\n  fetchImpl?: typeof fetch;\n  minValueBase?: string;\n  onAppeared: (payload: MempoolWatchAppearedPayload) => void;\n  onBusy?: () => void;\n  onError?: (error: Error) => void;\n  onTimeout: (payload: MempoolWatchTimeoutPayload) => void;\n  signal?: AbortSignal;\n}\n\nexport interface WatchMempoolHandle {\n  close: () => void;\n}\n\ninterface SseFrame {\n  data: string;\n  event: string;\n}\n\nconst BASE_UNITS_PATTERN = /^(?:0|[1-9]\\d*)$/u;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst parseSseFrame = (raw: string): SseFrame | null => {\n  const lines = raw.split(/\\r?\\n/u);\n  let event = \"message\";\n  const dataLines: string[] = [];\n\n  for (const line of lines) {\n    if (line.length === 0 || line.startsWith(\":\")) {\n      continue;\n    }\n\n    const separatorIndex = line.indexOf(\":\");\n    const field = separatorIndex === -1 ? line : line.slice(0, separatorIndex);\n    let value = separatorIndex === -1 ? \"\" : line.slice(separatorIndex + 1);\n    if (value.startsWith(\" \")) {\n      value = value.slice(1);\n    }\n\n    if (field === \"event\") {\n      event = value;\n    } else if (field === \"data\") {\n      dataLines.push(value);\n    }\n  }\n\n  if (dataLines.length === 0) {\n    return null;\n  }\n\n  return {\n    data: dataLines.join(\"\\n\"),\n    event,\n  };\n};\n\n/**\n * Convert a positive DOGE decimal string into base units\n * (1 DOGE = 100000000) as a non-negative integer string.\n */\nexport const dogeAmountToBaseUnits = (amount: string): string => {\n  const normalized = normalizeDogecoinAmount(amount);\n  const [wholePart, fractionalPart = \"\"] = normalized.split(\".\");\n  const fractional = fractionalPart.padEnd(8, \"0\");\n  const baseUnits =\n    Number(wholePart) * DOGE_BASE_UNITS_PER_COIN + Number(fractional);\n\n  if (!Number.isSafeInteger(baseUnits)) {\n    throw new MempoolWatchError(\n      \"Amount is too large to convert to base units exactly.\"\n    );\n  }\n\n  return String(baseUnits);\n};\n\nexport const isNonNegativeBaseUnits = (value: string): boolean =>\n  BASE_UNITS_PATTERN.test(value);\n\nexport const parseMempoolWatchAppearedPayload = (\n  data: string\n): MempoolWatchAppearedPayload => {\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(data) as unknown;\n  } catch {\n    throw new MempoolWatchError(\"Appeared event payload is not valid JSON.\");\n  }\n\n  if (!isRecord(parsed)) {\n    throw new MempoolWatchError(\"Appeared event payload must be an object.\");\n  }\n\n  const { address, detectedAt, outputs, source, txid } = parsed;\n\n  if (typeof address !== \"string\" || !isDogecoinP2pkhAddress(address)) {\n    throw new MempoolWatchError(\"Appeared event is missing a valid address.\");\n  }\n\n  if (typeof txid !== \"string\" || txid.length === 0) {\n    throw new MempoolWatchError(\"Appeared event is missing a txid.\");\n  }\n\n  if (typeof detectedAt !== \"string\" || detectedAt.length === 0) {\n    throw new MempoolWatchError(\"Appeared event is missing detectedAt.\");\n  }\n\n  if (source !== \"catchup\" && source !== \"live\") {\n    throw new MempoolWatchError(\"Appeared event has an invalid source.\");\n  }\n\n  if (!Array.isArray(outputs) || outputs.length === 0) {\n    throw new MempoolWatchError(\"Appeared event is missing outputs.\");\n  }\n\n  const normalizedOutputs = outputs.map((output, index) => {\n    if (!isRecord(output)) {\n      throw new MempoolWatchError(\n        `Appeared event output at index ${index} must be an object.`\n      );\n    }\n\n    if (typeof output.vout !== \"number\" || !Number.isInteger(output.vout)) {\n      throw new MempoolWatchError(\n        `Appeared event output at index ${index} has an invalid vout.`\n      );\n    }\n\n    if (\n      typeof output.valueBase !== \"string\" ||\n      !isNonNegativeBaseUnits(output.valueBase)\n    ) {\n      throw new MempoolWatchError(\n        `Appeared event output at index ${index} has an invalid valueBase.`\n      );\n    }\n\n    return {\n      valueBase: output.valueBase,\n      vout: output.vout,\n    };\n  });\n\n  return {\n    address,\n    detectedAt,\n    outputs: normalizedOutputs,\n    source,\n    txid,\n  };\n};\n\nexport const parseMempoolWatchTimeoutPayload = (\n  data: string\n): MempoolWatchTimeoutPayload => {\n  let parsed: unknown;\n  try {\n    parsed = JSON.parse(data) as unknown;\n  } catch {\n    throw new MempoolWatchError(\"Timeout event payload is not valid JSON.\");\n  }\n\n  if (!isRecord(parsed)) {\n    throw new MempoolWatchError(\"Timeout event payload must be an object.\");\n  }\n\n  const { address, expiresAt } = parsed;\n\n  if (typeof address !== \"string\" || !isDogecoinP2pkhAddress(address)) {\n    throw new MempoolWatchError(\"Timeout event is missing a valid address.\");\n  }\n\n  if (typeof expiresAt !== \"string\" || expiresAt.length === 0) {\n    throw new MempoolWatchError(\"Timeout event is missing expiresAt.\");\n  }\n\n  return { address, expiresAt };\n};\n\nexport const buildMempoolWatchEndpointUrl = (\n  endpoint: string,\n  address: string,\n  minValueBase?: string\n): string => {\n  const url = new URL(endpoint, \"http://localhost\");\n  const isAbsolute = /^https?:\\/\\//iu.test(endpoint);\n  url.searchParams.set(\"address\", address);\n  if (minValueBase !== undefined) {\n    url.searchParams.set(\"minValueBase\", minValueBase);\n  }\n\n  if (isAbsolute) {\n    return url.toString();\n  }\n\n  return `${url.pathname}${url.search}`;\n};\n\n/**\n * Parse Server-Sent Events frames, honoring named `event:` fields.\n * Comment lines (`:`) and keep-alives are ignored.\n */\nexport const parseSseChunk = (\n  buffer: string,\n  chunk: string\n): { frames: SseFrame[]; remaining: string } => {\n  const combined = `${buffer}${chunk}`;\n  const parts = combined.split(/\\r?\\n\\r?\\n/u);\n  const remaining = parts.pop() ?? \"\";\n  const frames: SseFrame[] = [];\n\n  for (const part of parts) {\n    const frame = parseSseFrame(part);\n    if (frame) {\n      frames.push(frame);\n    }\n  }\n\n  return { frames, remaining };\n};\n\ntype WatchOutcome =\n  | { type: \"appeared\"; payload: MempoolWatchAppearedPayload }\n  | { type: \"timeout\"; payload: MempoolWatchTimeoutPayload }\n  | { type: \"busy\" }\n  | { type: \"error\"; error: Error }\n  | { type: \"closed\" };\n\nconst dispatchOutcome = (\n  options: WatchMempoolOptions,\n  outcome: WatchOutcome\n): void => {\n  switch (outcome.type) {\n    case \"appeared\": {\n      options.onAppeared(outcome.payload);\n      break;\n    }\n    case \"timeout\": {\n      options.onTimeout(outcome.payload);\n      break;\n    }\n    case \"busy\": {\n      options.onBusy?.();\n      break;\n    }\n    case \"error\": {\n      options.onError?.(outcome.error);\n      break;\n    }\n    default: {\n      break;\n    }\n  }\n};\n\nconst readWatchOutcome = async (\n  response: Response,\n  shouldStop: () => boolean\n): Promise<WatchOutcome> => {\n  if (response.status === 409) {\n    return { type: \"busy\" };\n  }\n\n  if (!response.ok) {\n    return {\n      error: new MempoolWatchError(\n        `Mempool watch failed with status ${response.status}.`\n      ),\n      type: \"error\",\n    };\n  }\n\n  if (!response.body) {\n    return {\n      error: new MempoolWatchError(\"Mempool watch response had no body.\"),\n      type: \"error\",\n    };\n  }\n\n  const reader = response.body.getReader();\n  const decoder = new TextDecoder();\n  let buffer = \"\";\n\n  while (!shouldStop()) {\n    const { done, value } = await reader.read();\n    if (done) {\n      break;\n    }\n\n    const decoded = decoder.decode(value, { stream: true });\n    const parsed = parseSseChunk(buffer, decoded);\n    buffer = parsed.remaining;\n\n    for (const frame of parsed.frames) {\n      if (frame.event === MEMPOOL_WATCH_APPEARED_EVENT) {\n        try {\n          return {\n            payload: parseMempoolWatchAppearedPayload(frame.data),\n            type: \"appeared\",\n          };\n        } catch (error) {\n          return {\n            error:\n              error instanceof Error\n                ? error\n                : new MempoolWatchError(\"Failed to parse appeared event.\"),\n            type: \"error\",\n          };\n        }\n      }\n\n      if (frame.event === MEMPOOL_WATCH_TIMEOUT_EVENT) {\n        try {\n          return {\n            payload: parseMempoolWatchTimeoutPayload(frame.data),\n            type: \"timeout\",\n          };\n        } catch (error) {\n          return {\n            error:\n              error instanceof Error\n                ? error\n                : new MempoolWatchError(\"Failed to parse timeout event.\"),\n            type: \"error\",\n          };\n        }\n      }\n    }\n  }\n\n  if (shouldStop()) {\n    return { type: \"closed\" };\n  }\n\n  return {\n    error: new MempoolWatchError(\n      \"Mempool watch ended before a payment appeared.\"\n    ),\n    type: \"error\",\n  };\n};\n\n/**\n * Open a one-shot SSE mempool watch session for a receive address.\n * Uses fetch streaming so HTTP statuses like 409 are observable while still\n * dispatching on named `event:` frames.\n */\nexport const watchMempoolPayment = (\n  options: WatchMempoolOptions\n): WatchMempoolHandle => {\n  const {\n    address,\n    endpoint,\n    fetchImpl = fetch,\n    minValueBase,\n    signal,\n  } = options;\n\n  if (!isDogecoinP2pkhAddress(address)) {\n    throw new MempoolWatchError(\"Cannot watch an invalid Dogecoin address.\");\n  }\n\n  if (minValueBase !== undefined && !isNonNegativeBaseUnits(minValueBase)) {\n    throw new MempoolWatchError(\n      \"minValueBase must be a non-negative integer string.\"\n    );\n  }\n\n  const controller = new AbortController();\n  let settled = false;\n\n  const close = () => {\n    if (settled) {\n      return;\n    }\n    settled = true;\n    controller.abort();\n  };\n\n  if (signal) {\n    if (signal.aborted) {\n      close();\n      return { close };\n    }\n    signal.addEventListener(\"abort\", close, { once: true });\n  }\n\n  const url = buildMempoolWatchEndpointUrl(endpoint, address, minValueBase);\n\n  void (async () => {\n    try {\n      const response = await fetchImpl(url, {\n        headers: {\n          Accept: \"text/event-stream\",\n          \"Cache-Control\": \"no-store\",\n        },\n        method: \"GET\",\n        signal: controller.signal,\n      });\n\n      if (settled) {\n        return;\n      }\n\n      const outcome = await readWatchOutcome(response, () => settled);\n      if (settled || outcome.type === \"closed\") {\n        return;\n      }\n\n      settled = true;\n      controller.abort();\n      dispatchOutcome(options, outcome);\n    } catch (error: unknown) {\n      if (\n        settled ||\n        controller.signal.aborted ||\n        (error instanceof DOMException && error.name === \"AbortError\")\n      ) {\n        return;\n      }\n\n      settled = true;\n      dispatchOutcome(options, {\n        error:\n          error instanceof Error\n            ? error\n            : new MempoolWatchError(\"Mempool watch connection failed.\"),\n        type: \"error\",\n      });\n    }\n  })();\n\n  return { close };\n};\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/mempool-watch.ts"
    },
    {
      "path": "registry/default/dogecoin-payment-button/payment-uri.ts",
      "content": "const AMOUNT_PATTERN = /^(?:0|[1-9]\\d*)(?:\\.\\d{1,8})?$/u;\n\nexport class PaymentUriError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"PaymentUriError\";\n  }\n}\n\n/**\n * Validate a suggested DOGE amount as a positive decimal string with at most\n * 8 fractional digits.\n */\nexport const normalizeDogecoinAmount = (amount: string): string => {\n  const trimmed = amount.trim();\n\n  if (!AMOUNT_PATTERN.test(trimmed)) {\n    throw new PaymentUriError(\n      \"Amount must be a positive decimal string with up to 8 decimal places.\"\n    );\n  }\n\n  const value = Number(trimmed);\n  if (!Number.isFinite(value) || value <= 0) {\n    throw new PaymentUriError(\"Amount must be greater than zero.\");\n  }\n\n  const [whole, fractional] = trimmed.split(\".\");\n  if (!fractional) {\n    return whole;\n  }\n\n  const normalizedFractional = fractional.replace(/0+$/u, \"\");\n  return normalizedFractional ? `${whole}.${normalizedFractional}` : whole;\n};\n\nexport const buildDogecoinPaymentUri = (\n  address: string,\n  amount?: string\n): string => {\n  if (!amount) {\n    return `dogecoin:${address}`;\n  }\n\n  const normalized = normalizeDogecoinAmount(amount);\n  return `dogecoin:${address}?amount=${normalized}`;\n};\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/payment-uri.ts"
    },
    {
      "path": "registry/default/dogecoin-payment-button/resolve-address.ts",
      "content": "import type {\n  AddressSelectionResult,\n  AddressUsageChecker,\n} from \"./address-selection\";\nimport { selectPaymentAddress } from \"./address-selection\";\nimport { defaultAddressUsageChecker } from \"./blockcypher\";\nimport { derivePaymentAddress } from \"./dogecoin\";\n\nexport interface ResolveDogecoinPaymentAddressOptions {\n  addressIndexStep?: number;\n  checker?: AddressUsageChecker;\n  maxAttempts?: number;\n  signal?: AbortSignal;\n  startIndex?: number;\n  uncheckedMode?: boolean;\n  xpub: string;\n}\n\n/**\n * Resolve the next Payment Address for an account xpub.\n * Safe to call from a Next.js Server Action or the browser.\n */\nexport const resolveDogecoinPaymentAddress = (\n  options: ResolveDogecoinPaymentAddressOptions\n): Promise<AddressSelectionResult> =>\n  selectPaymentAddress({\n    addressIndexStep: options.addressIndexStep,\n    checker: options.checker ?? defaultAddressUsageChecker,\n    deriveAddress: (index) => derivePaymentAddress(options.xpub, index),\n    maxAttempts: options.maxAttempts,\n    signal: options.signal,\n    startIndex: options.startIndex,\n    uncheckedMode: options.uncheckedMode,\n  });\n\nexport type {\n  AddressSelectionResult,\n  AddressUsageChecker,\n  SelectionState,\n} from \"./address-selection\";\nexport { createInitialSelectionState } from \"./address-selection\";\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/resolve-address.ts"
    },
    {
      "path": "registry/default/dogecoin-payment-button/payment-button-types.ts",
      "content": "import type { ReactNode } from \"react\";\n\nimport type {\n  AddressSelectionResult,\n  AddressUsageChecker,\n  SelectionState,\n} from \"./address-selection\";\nimport type { MempoolWatchAppearedPayload } from \"./mempool-watch\";\n\nexport type DogecoinPaymentResolveFn = (\n  state: SelectionState\n) => Promise<AddressSelectionResult>;\n\nexport interface MempoolWatchConfig {\n  /** Same-origin or absolute SSE proxy endpoint. */\n  endpoint: string;\n  /**\n   * Optional fetch implementation for tests. Defaults to global fetch.\n   */\n  fetchImpl?: typeof fetch;\n}\n\ninterface SharedProps {\n  amount?: string;\n  buttonLabel?: string;\n  children?: ReactNode;\n  className?: string;\n  mempoolWatch?: MempoolWatchConfig;\n  onPaymentAppeared?: (payload: MempoolWatchAppearedPayload) => void;\n}\n\nexport interface AddressModeProps extends SharedProps {\n  address: string;\n  addressIndexStep?: never;\n  checker?: never;\n  maxAttempts?: never;\n  resolveAddress?: never;\n  xpub?: never;\n}\n\nexport interface ServerModeProps extends SharedProps {\n  address?: never;\n  addressIndexStep?: never;\n  checker?: never;\n  maxAttempts?: never;\n  resolveAddress: DogecoinPaymentResolveFn;\n  xpub?: never;\n}\n\nexport interface BrowserModeProps extends SharedProps {\n  address?: never;\n  addressIndexStep?: number;\n  checker?: AddressUsageChecker;\n  maxAttempts?: number;\n  resolveAddress?: never;\n  xpub: string;\n}\n\nexport type DogecoinPaymentButtonProps =\n  | AddressModeProps\n  | ServerModeProps\n  | BrowserModeProps;\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/payment-button-types.ts"
    },
    {
      "path": "registry/default/dogecoin-payment-button/payment-button-parts.tsx",
      "content": "\"use client\";\n\nimport { CheckIcon, CopyIcon, LoaderCircleIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { renderSVG } from \"uqr\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nimport type {\n  MempoolWatchAppearedPayload,\n  MempoolWatchStatus,\n} from \"./mempool-watch\";\nimport { watchStatusClassName } from \"./watch-status\";\n\nexport interface ResolvedPayment {\n  address: string;\n  uri: string;\n}\n\ninterface DogecoinQrCodeProps {\n  value: string;\n}\n\nconst DogecoinQrCode = ({ value }: DogecoinQrCodeProps) => {\n  const svg = React.useMemo(() => renderSVG(value, { border: 2 }), [value]);\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      className=\"mx-auto size-48 overflow-hidden rounded-lg bg-white p-2 text-black [&_svg]:size-full\"\n      dangerouslySetInnerHTML={{ __html: svg }}\n    />\n  );\n};\n\ninterface CopyAddressButtonProps {\n  address: string;\n}\n\nconst CopyAddressButton = ({ address }: CopyAddressButtonProps) => {\n  const [copied, setCopied] = React.useState(false);\n  const timeoutRef = React.useRef<number | null>(null);\n\n  React.useEffect(\n    () => () => {\n      if (timeoutRef.current !== null) {\n        window.clearTimeout(timeoutRef.current);\n      }\n    },\n    []\n  );\n\n  const handleCopy = async () => {\n    setCopied(true);\n    if (timeoutRef.current !== null) {\n      window.clearTimeout(timeoutRef.current);\n    }\n    timeoutRef.current = window.setTimeout(() => {\n      setCopied(false);\n    }, 1600);\n\n    try {\n      await navigator.clipboard.writeText(address);\n    } catch {\n      // Keep the copied feedback even if the Clipboard API is unavailable.\n    }\n  };\n\n  return (\n    <Button\n      aria-label={copied ? \"Address copied\" : \"Copy address\"}\n      className=\"w-full\"\n      onClick={() => {\n        void handleCopy();\n      }}\n      type=\"button\"\n      variant=\"outline\"\n    >\n      <span className=\"relative inline-flex size-4 items-center justify-center\">\n        <CopyIcon\n          className={cn(\n            \"absolute size-4 transition-all duration-200\",\n            copied ? \"scale-50 opacity-0\" : \"scale-100 opacity-100\"\n          )}\n        />\n        <CheckIcon\n          className={cn(\n            \"absolute size-4 text-emerald-600 transition-all duration-200\",\n            copied ? \"scale-100 opacity-100\" : \"scale-50 opacity-0\"\n          )}\n        />\n      </span>\n      <span className=\"relative inline-grid overflow-hidden\">\n        <span\n          className={cn(\n            \"col-start-1 row-start-1 transition-all duration-200\",\n            copied ? \"-translate-y-full opacity-0\" : \"translate-y-0 opacity-100\"\n          )}\n        >\n          Copy address\n        </span>\n        <span\n          aria-live=\"polite\"\n          className={cn(\n            \"col-start-1 row-start-1 transition-all duration-200\",\n            copied ? \"translate-y-0 opacity-100\" : \"translate-y-full opacity-0\"\n          )}\n        >\n          Copied\n        </span>\n      </span>\n    </Button>\n  );\n};\n\ninterface MempoolWatchBannerProps {\n  appearedPayload: MempoolWatchAppearedPayload | null;\n  message: string;\n  onRetry: () => void;\n  status: MempoolWatchStatus;\n}\n\nconst MempoolWatchBanner = ({\n  appearedPayload,\n  message,\n  onRetry,\n  status,\n}: MempoolWatchBannerProps) => {\n  const canRetry =\n    status === \"timeout\" || status === \"busy\" || status === \"error\";\n\n  return (\n    <div\n      aria-live=\"polite\"\n      className={cn(\n        \"rounded-lg border p-3 text-sm\",\n        watchStatusClassName(status)\n      )}\n    >\n      <div className=\"flex items-start gap-2\">\n        {status === \"watching\" ? (\n          <LoaderCircleIcon className=\"mt-0.5 size-4 shrink-0 animate-spin\" />\n        ) : null}\n        {status === \"appeared\" ? (\n          <CheckIcon className=\"mt-0.5 size-4 shrink-0\" />\n        ) : null}\n        <div className=\"space-y-1\">\n          <p>{message}</p>\n          {appearedPayload ? (\n            <p className=\"break-all font-mono text-xs opacity-80\">\n              txid: {appearedPayload.txid}\n            </p>\n          ) : null}\n        </div>\n      </div>\n\n      {canRetry ? (\n        <Button\n          className=\"mt-3\"\n          onClick={onRetry}\n          type=\"button\"\n          variant=\"outline\"\n        >\n          Watch again\n        </Button>\n      ) : null}\n    </div>\n  );\n};\n\ninterface PaymentDetailsProps {\n  amount?: string;\n  appearedPayload: MempoolWatchAppearedPayload | null;\n  payment: ResolvedPayment;\n  watchMessage: string | null;\n  watchStatus: MempoolWatchStatus;\n  onRetryWatch: () => void;\n}\n\nconst PaymentDetails = ({\n  amount,\n  appearedPayload,\n  payment,\n  watchMessage,\n  watchStatus,\n  onRetryWatch,\n}: PaymentDetailsProps) => (\n  <>\n    {amount ? (\n      <p className=\"text-center text-sm text-muted-foreground\">\n        Suggested amount:{\" \"}\n        <span className=\"font-medium text-foreground\">{amount} DOGE</span>\n      </p>\n    ) : null}\n\n    <DogecoinQrCode value={payment.uri} />\n\n    <div className=\"rounded-lg border bg-muted/40 px-3 py-2\">\n      <p className=\"break-all font-mono text-xs leading-relaxed\">\n        {payment.address}\n      </p>\n    </div>\n\n    <CopyAddressButton address={payment.address} />\n\n    {watchMessage ? (\n      <MempoolWatchBanner\n        appearedPayload={appearedPayload}\n        message={watchMessage}\n        onRetry={onRetryWatch}\n        status={watchStatus}\n      />\n    ) : null}\n  </>\n);\n\nexport interface PaymentDialogBodyProps {\n  amount?: string;\n  appearedPayload: MempoolWatchAppearedPayload | null;\n  errorMessage: string | null;\n  mempoolWatchEnabled: boolean;\n  payment: ResolvedPayment | null;\n  status: \"idle\" | \"loading\" | \"error\";\n  watchMessage: string | null;\n  watchStatus: MempoolWatchStatus;\n  onRetryOpen: () => void;\n  onRetryWatch: () => void;\n}\n\nexport const PaymentDialogBody = ({\n  amount,\n  appearedPayload,\n  errorMessage,\n  mempoolWatchEnabled,\n  payment,\n  status,\n  watchMessage,\n  watchStatus,\n  onRetryOpen,\n  onRetryWatch,\n}: PaymentDialogBodyProps) => (\n  <div className=\"flex flex-col gap-4\">\n    {status === \"loading\" ? (\n      <div className=\"flex min-h-48 flex-col items-center justify-center gap-3 text-muted-foreground\">\n        <LoaderCircleIcon className=\"size-6 animate-spin\" />\n        <p>Finding a payment address…</p>\n      </div>\n    ) : null}\n\n    {status === \"error\" ? (\n      <div className=\"rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive\">\n        <p>{errorMessage ?? \"Something went wrong.\"}</p>\n        <Button\n          className=\"mt-3\"\n          onClick={onRetryOpen}\n          type=\"button\"\n          variant=\"outline\"\n        >\n          Try again\n        </Button>\n      </div>\n    ) : null}\n\n    {status === \"idle\" && payment ? (\n      <PaymentDetails\n        amount={amount}\n        appearedPayload={appearedPayload}\n        onRetryWatch={onRetryWatch}\n        payment={payment}\n        watchMessage={mempoolWatchEnabled ? watchMessage : null}\n        watchStatus={watchStatus}\n      />\n    ) : null}\n  </div>\n);\n",
      "type": "registry:component",
      "target": "components/dogecoin-payment-button/payment-button-parts.tsx"
    },
    {
      "path": "registry/default/dogecoin-payment-button/use-payment-button-state.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type {\n  AddressSelectionResult,\n  AddressUsageChecker,\n  SelectionState,\n} from \"./address-selection\";\nimport { createInitialSelectionState } from \"./address-selection\";\nimport type {\n  MempoolWatchAppearedPayload,\n  MempoolWatchStatus,\n  WatchMempoolHandle,\n} from \"./mempool-watch\";\nimport { dogeAmountToBaseUnits, watchMempoolPayment } from \"./mempool-watch\";\nimport type { ResolvedPayment } from \"./payment-button-parts\";\nimport type { DogecoinPaymentResolveFn } from \"./payment-button-types\";\nimport { buildDogecoinPaymentUri } from \"./payment-uri\";\nimport { resolveDogecoinPaymentAddress } from \"./resolve-address\";\n\ntype ResolutionStatus = \"idle\" | \"loading\" | \"error\";\n\ninterface ResolutionState {\n  errorMessage: string | null;\n  payment: ResolvedPayment | null;\n  selectionState: SelectionState;\n  status: ResolutionStatus;\n}\n\ntype ResolutionAction =\n  | { type: \"static-ready\"; payment: ResolvedPayment }\n  | { type: \"resolve-start\" }\n  | {\n      type: \"resolve-success\";\n      payment: ResolvedPayment;\n      selectionState: SelectionState;\n    }\n  | { type: \"resolve-error\"; message: string };\n\nconst createInitialResolutionState = (\n  staticAddress: string | undefined,\n  amount: string | undefined\n): ResolutionState => ({\n  errorMessage: null,\n  payment: staticAddress\n    ? {\n        address: staticAddress,\n        uri: buildDogecoinPaymentUri(staticAddress, amount),\n      }\n    : null,\n  selectionState: createInitialSelectionState(),\n  status: \"idle\",\n});\n\nconst resolutionReducer = (\n  state: ResolutionState,\n  action: ResolutionAction\n): ResolutionState => {\n  switch (action.type) {\n    case \"static-ready\": {\n      return {\n        ...state,\n        errorMessage: null,\n        payment: action.payment,\n        status: \"idle\",\n      };\n    }\n    case \"resolve-start\": {\n      return {\n        ...state,\n        errorMessage: null,\n        status: \"loading\",\n      };\n    }\n    case \"resolve-success\": {\n      return {\n        ...state,\n        errorMessage: null,\n        payment: action.payment,\n        selectionState: action.selectionState,\n        status: \"idle\",\n      };\n    }\n    case \"resolve-error\": {\n      return {\n        ...state,\n        errorMessage: action.message,\n        status: \"error\",\n      };\n    }\n    default: {\n      return state;\n    }\n  }\n};\n\ninterface WatchState {\n  appearedPayload: MempoolWatchAppearedPayload | null;\n  errorMessage: string | null;\n  session: number;\n  status: MempoolWatchStatus;\n}\n\ntype WatchAction =\n  | { type: \"reset\" }\n  | { type: \"start\" }\n  | { type: \"amount-error\"; message: string }\n  | { type: \"appeared\"; payload: MempoolWatchAppearedPayload }\n  | { type: \"busy\" }\n  | { type: \"error\"; message: string }\n  | { type: \"timeout\" }\n  | { type: \"retry\" };\n\nconst initialWatchState: WatchState = {\n  appearedPayload: null,\n  errorMessage: null,\n  session: 0,\n  status: \"idle\",\n};\n\nconst watchReducer = (state: WatchState, action: WatchAction): WatchState => {\n  switch (action.type) {\n    case \"reset\": {\n      return {\n        ...state,\n        appearedPayload: null,\n        errorMessage: null,\n        status: \"idle\",\n      };\n    }\n    case \"start\": {\n      return {\n        ...state,\n        appearedPayload: null,\n        errorMessage: null,\n        status: \"watching\",\n      };\n    }\n    case \"amount-error\": {\n      return {\n        ...state,\n        errorMessage: action.message,\n        status: \"error\",\n      };\n    }\n    case \"appeared\": {\n      return {\n        ...state,\n        appearedPayload: action.payload,\n        status: \"appeared\",\n      };\n    }\n    case \"busy\": {\n      return {\n        ...state,\n        status: \"busy\",\n      };\n    }\n    case \"error\": {\n      return {\n        ...state,\n        errorMessage: action.message,\n        status: \"error\",\n      };\n    }\n    case \"timeout\": {\n      return {\n        ...state,\n        status: \"timeout\",\n      };\n    }\n    case \"retry\": {\n      return {\n        appearedPayload: null,\n        errorMessage: null,\n        session: state.session + 1,\n        status: \"idle\",\n      };\n    }\n    default: {\n      return state;\n    }\n  }\n};\n\nexport interface UsePaymentResolutionOptions {\n  address?: string;\n  addressIndexStep?: number;\n  amount?: string;\n  checker?: AddressUsageChecker;\n  maxAttempts?: number;\n  open: boolean;\n  resolveAddress?: DogecoinPaymentResolveFn;\n  xpub?: string;\n}\n\nexport const usePaymentResolution = ({\n  address: staticAddress,\n  addressIndexStep,\n  amount,\n  checker,\n  maxAttempts,\n  open,\n  resolveAddress,\n  xpub,\n}: UsePaymentResolutionOptions) => {\n  const [state, dispatch] = React.useReducer(resolutionReducer, undefined, () =>\n    createInitialResolutionState(staticAddress, amount)\n  );\n  const requestIdRef = React.useRef(0);\n  const abortRef = React.useRef<AbortController | null>(null);\n  const selectionStateRef = React.useRef(state.selectionState);\n\n  React.useEffect(() => {\n    selectionStateRef.current = state.selectionState;\n  }, [state.selectionState]);\n\n  const runResolution = React.useCallback(\n    async (\n      selection: SelectionState,\n      signal: AbortSignal,\n      requestId: number\n    ) => {\n      let result: AddressSelectionResult;\n\n      if (resolveAddress) {\n        result = await resolveAddress(selection);\n      } else if (xpub) {\n        result = await resolveDogecoinPaymentAddress({\n          addressIndexStep,\n          checker,\n          maxAttempts,\n          signal,\n          startIndex: selection.startIndex,\n          uncheckedMode: selection.uncheckedMode,\n          xpub,\n        });\n      } else {\n        throw new Error(\"No payment address source configured.\");\n      }\n\n      if (requestId !== requestIdRef.current || signal.aborted) {\n        return;\n      }\n\n      dispatch({\n        payment: {\n          address: result.address,\n          uri: buildDogecoinPaymentUri(result.address, amount),\n        },\n        selectionState: {\n          startIndex: result.nextStartIndex,\n          uncheckedMode: result.uncheckedMode,\n        },\n        type: \"resolve-success\",\n      });\n    },\n    [amount, addressIndexStep, checker, maxAttempts, resolveAddress, xpub]\n  );\n\n  React.useEffect(() => {\n    if (!open) {\n      abortRef.current?.abort();\n      abortRef.current = null;\n      return;\n    }\n\n    if (staticAddress) {\n      const nextUri = buildDogecoinPaymentUri(staticAddress, amount);\n      dispatch({\n        payment: {\n          address: staticAddress,\n          uri: nextUri,\n        },\n        type: \"static-ready\",\n      });\n      return;\n    }\n\n    const requestId = requestIdRef.current + 1;\n    requestIdRef.current = requestId;\n    const controller = new AbortController();\n    abortRef.current = controller;\n    const stateSnapshot = selectionStateRef.current;\n\n    dispatch({ type: \"resolve-start\" });\n\n    void (async () => {\n      try {\n        await runResolution(stateSnapshot, controller.signal, requestId);\n      } catch (error: unknown) {\n        if (\n          controller.signal.aborted ||\n          requestId !== requestIdRef.current ||\n          (error instanceof DOMException && error.name === \"AbortError\")\n        ) {\n          return;\n        }\n\n        dispatch({\n          message:\n            error instanceof Error\n              ? error.message\n              : \"Unable to resolve a Dogecoin payment address.\",\n          type: \"resolve-error\",\n        });\n      }\n    })();\n\n    return () => {\n      controller.abort();\n    };\n  }, [amount, open, runResolution, staticAddress]);\n\n  return state;\n};\n\nexport interface UseMempoolWatchOptions {\n  amount?: string;\n  endpoint?: string;\n  fetchImpl?: typeof fetch;\n  onPaymentAppeared?: (payload: MempoolWatchAppearedPayload) => void;\n  open: boolean;\n  paymentAddress?: string;\n  resolutionStatus: ResolutionStatus;\n}\n\nexport const useMempoolWatch = ({\n  amount,\n  endpoint,\n  fetchImpl,\n  onPaymentAppeared,\n  open,\n  paymentAddress,\n  resolutionStatus,\n}: UseMempoolWatchOptions) => {\n  const [state, dispatch] = React.useReducer(watchReducer, initialWatchState);\n  const watchHandleRef = React.useRef<WatchMempoolHandle | null>(null);\n  const appearedCallbackRef = React.useRef(onPaymentAppeared);\n\n  React.useEffect(() => {\n    appearedCallbackRef.current = onPaymentAppeared;\n  }, [onPaymentAppeared]);\n\n  React.useEffect(() => {\n    watchHandleRef.current?.close();\n    watchHandleRef.current = null;\n\n    if (!open) {\n      dispatch({ type: \"reset\" });\n      return;\n    }\n\n    if (!endpoint || !paymentAddress || resolutionStatus !== \"idle\") {\n      return;\n    }\n\n    let minValueBase: string | undefined;\n    if (amount) {\n      try {\n        minValueBase = dogeAmountToBaseUnits(amount);\n      } catch (error: unknown) {\n        dispatch({\n          message:\n            error instanceof Error\n              ? error.message\n              : \"Unable to convert the suggested amount to base units.\",\n          type: \"amount-error\",\n        });\n        return;\n      }\n    }\n\n    let active = true;\n    dispatch({ type: \"start\" });\n\n    const controller = new AbortController();\n    const handle = watchMempoolPayment({\n      address: paymentAddress,\n      endpoint,\n      fetchImpl,\n      minValueBase,\n      onAppeared: (payload) => {\n        if (!active) {\n          return;\n        }\n        dispatch({ payload, type: \"appeared\" });\n        appearedCallbackRef.current?.(payload);\n      },\n      onBusy: () => {\n        if (!active) {\n          return;\n        }\n        dispatch({ type: \"busy\" });\n      },\n      onError: (error) => {\n        if (!active) {\n          return;\n        }\n        dispatch({ message: error.message, type: \"error\" });\n      },\n      onTimeout: () => {\n        if (!active) {\n          return;\n        }\n        dispatch({ type: \"timeout\" });\n      },\n      signal: controller.signal,\n    });\n    watchHandleRef.current = handle;\n\n    return () => {\n      active = false;\n      controller.abort();\n      handle.close();\n      if (watchHandleRef.current === handle) {\n        watchHandleRef.current = null;\n      }\n    };\n  }, [\n    amount,\n    endpoint,\n    fetchImpl,\n    open,\n    paymentAddress,\n    resolutionStatus,\n    state.session,\n  ]);\n\n  const retryWatch = () => {\n    dispatch({ type: \"retry\" });\n  };\n\n  return {\n    appearedPayload: state.appearedPayload,\n    errorMessage: state.errorMessage,\n    retryWatch,\n    status: state.status,\n  };\n};\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/use-payment-button-state.ts"
    },
    {
      "path": "registry/default/dogecoin-payment-button/watch-status.ts",
      "content": "import type { MempoolWatchStatus } from \"./mempool-watch\";\n\nexport const watchStatusMessage = (\n  status: MempoolWatchStatus,\n  errorMessage: string | null\n): string | null => {\n  switch (status) {\n    case \"watching\": {\n      return \"Waiting for payment in the mempool…\";\n    }\n    case \"appeared\": {\n      return \"Payment detected in the mempool (not yet confirmed).\";\n    }\n    case \"timeout\": {\n      return \"No matching payment appeared within 5 minutes.\";\n    }\n    case \"busy\": {\n      return \"Another mempool watch session is already open. Try again shortly.\";\n    }\n    case \"error\": {\n      return errorMessage ?? \"Unable to watch the mempool for this payment.\";\n    }\n    default: {\n      return null;\n    }\n  }\n};\n\nexport const watchStatusClassName = (status: MempoolWatchStatus): string => {\n  if (status === \"appeared\") {\n    return \"border-emerald-500/30 bg-emerald-500/10 text-emerald-800 dark:text-emerald-300\";\n  }\n  if (status === \"watching\") {\n    return \"border-border bg-muted/40 text-muted-foreground\";\n  }\n  return \"border-destructive/30 bg-destructive/5 text-destructive\";\n};\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/watch-status.ts"
    },
    {
      "path": "registry/default/dogecoin-payment-button/index.ts",
      "content": "export {\n  DEFAULT_ADDRESS_INDEX_STEP,\n  DEFAULT_MAX_ATTEMPTS,\n  createInitialSelectionState,\n  selectPaymentAddress,\n} from \"./address-selection\";\nexport type {\n  AddressSelectionResult,\n  AddressUsageChecker,\n  SelectionMode,\n  SelectionState,\n} from \"./address-selection\";\nexport {\n  BLOCKCYPHER_DOGE_ADDRESS_URL,\n  createBlockCypherAddressUsageChecker,\n  defaultAddressUsageChecker,\n} from \"./blockcypher\";\nexport {\n  DogecoinKeyError,\n  derivePaymentAddress,\n  isDogecoinP2pkhAddress,\n  parseAccountXpub,\n} from \"./dogecoin\";\nexport { DogecoinPaymentButton } from \"./dogecoin-payment-button\";\nexport type {\n  DogecoinPaymentButtonProps,\n  DogecoinPaymentResolveFn,\n  MempoolWatchConfig,\n} from \"./payment-button-types\";\nexport {\n  DOGE_BASE_UNITS_PER_COIN,\n  MEMPOOL_WATCH_APPEARED_EVENT,\n  MEMPOOL_WATCH_TIMEOUT_EVENT,\n  MempoolWatchError,\n  buildMempoolWatchEndpointUrl,\n  dogeAmountToBaseUnits,\n  isNonNegativeBaseUnits,\n  parseMempoolWatchAppearedPayload,\n  parseMempoolWatchTimeoutPayload,\n  parseSseChunk,\n  watchMempoolPayment,\n} from \"./mempool-watch\";\nexport type {\n  MempoolWatchAppearedPayload,\n  MempoolWatchOutput,\n  MempoolWatchSource,\n  MempoolWatchStatus,\n  MempoolWatchTimeoutPayload,\n  WatchMempoolHandle,\n  WatchMempoolOptions,\n} from \"./mempool-watch\";\nexport {\n  buildDogecoinPaymentUri,\n  normalizeDogecoinAmount,\n} from \"./payment-uri\";\nexport { resolveDogecoinPaymentAddress } from \"./resolve-address\";\n",
      "type": "registry:lib",
      "target": "components/dogecoin-payment-button/index.ts"
    }
  ],
  "envVars": {
    "DOGECOIN_XPUB": "",
    "ONLYDOGE_API_TOKEN": "",
    "ONLYDOGE_API_BASE_URL": "https://platform.onlydoge.io"
  },
  "docs": "Set DOGECOIN_XPUB to a dedicated Dogecoin BIP44 account public key at m/44'/3'/0' (dgub or xpub). Prefer resolving addresses in a Server Action and passing only the address (or a resolver) to the client. Browser-side xpub derivation exposes the full account receive chain to visitors. For optional mempool watching, keep ONLYDOGE_API_TOKEN server-side and proxy SSE through a same-origin route; pass mempoolWatch.endpoint to the button.",
  "type": "registry:block"
}