stacksdapp-codegen 0.2.0

TypeScript code generation engine for Stacks dApps, using Tera templates to create type-safe contract hooks.
Documentation
// AUTO-GENERATED by scaffold-stacks. DO NOT EDIT.
// Re-run `stacksdapp generate` to update.

"use client";
import { useState, useCallback, useEffect } from 'react';
import { ClarityValue } from '@stacks/transactions';
import * as contracts from './contracts';
import { scaffoldConfig } from '../scaffold.config';

type TxLifecycleStatus = 'pending' | 'success' | 'abort_by_response' | 'error';

const TX_POLL_INITIAL_DELAY_MS = 1500;
const TX_POLL_INTERVAL_MS = 2500;
const TX_POLL_MAX_TRANSIENT_ERRORS = 20;

/** Hiro may return 404 until the tx is indexed — not a failure. */
function isTransientTxPollHttpStatus(status: number): boolean {
  return status === 404 || status === 408 || status === 429 || status === 502 || status === 503 || status === 504;
}

{% for contract in contracts %}
{% for fn in contract.functions %}
{% if fn.access == "public" or fn.access == "read_only" %}
export function use{{ contract.contract_name | upper_camel }}_{{ fn.name | upper_camel }}() {
  const [data, setData] = useState<unknown>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const [txid, setTxid] = useState<string | null>(null);
  const [txStatus, setTxStatus] = useState<TxLifecycleStatus | null>(null);
  const [txStatusError, setTxStatusError] = useState<string | null>(null);

  
  const isReadOnly = {% if fn.access == "read_only" %}true{% else %}false{% endif %};

  const call = useCallback(async (functionArgs: ClarityValue[] = []) => {
    setLoading(true);
    setError(null);
    setTxid(null);
    setTxStatus(null);
    setTxStatusError(null);
    try {
      // Use the generated contract function from the index
      const fn = (contracts as any).{{ contract.contract_name | camel }}_{{ fn.name | camel }};
      
      const result = isReadOnly 
        ? await fn(functionArgs) 
        : await fn(functionArgs, []); // Default empty post-conditions for public

      if (!isReadOnly && result?.txid) {
        setTxid(result.txid);
        setTxStatus('pending');
      }
      
      setData(result ?? null);
      return result;
    } catch (e) {
      console.error(e);
      setError(e as Error);
      throw e; // Re-throw so the caller can handle it if needed
    } finally {
      setLoading(false);
    }
    // Dependency array is stable because isReadOnly is a constant literal
  }, [isReadOnly]); 

  useEffect(() => {
    if (isReadOnly || !txid || !scaffoldConfig.nodeUrl) return;

    let mounted = true;
    let timer: ReturnType<typeof setTimeout> | null = null;
    let transientErrors = 0;
    const headers: Record<string, string> = {};
    if (scaffoldConfig.hiroApiKey) {
      headers['x-api-key'] = scaffoldConfig.hiroApiKey;
    }

    const scheduleRetry = () => {
      timer = setTimeout(() => {
        void poll();
      }, TX_POLL_INTERVAL_MS);
    };

    const poll = async () => {
      try {
        const response = await fetch(
          `${scaffoldConfig.nodeUrl}/extended/v1/tx/${txid}`,
          {
            headers,
            cache: 'no-store',
          },
        );
        if (!response.ok) {
          if (isTransientTxPollHttpStatus(response.status)) {
            transientErrors += 1;
            if (!mounted) return;
            if (transientErrors >= TX_POLL_MAX_TRANSIENT_ERRORS) {
              setTxStatus('error');
              setTxStatusError(
                `Transaction status unavailable (HTTP ${response.status}) after ${transientErrors} attempts.`,
              );
              return;
            }
            setTxStatus('pending');
            setTxStatusError(null);
            scheduleRetry();
            return;
          }
          throw new Error(`tx status request failed with ${response.status}`);
        }
        transientErrors = 0;
        const payload = await response.json();
        const status = String(payload?.tx_status ?? '').toLowerCase();
        if (!mounted) return;

        if (status.includes('success')) {
          setTxStatus('success');
          setTxStatusError(null);
          return;
        }
        if (status.includes('abort')) {
          setTxStatus('abort_by_response');
          setTxStatusError(String(payload?.tx_result?.repr ?? 'Transaction aborted.'));
          return;
        }
        if (status.includes('pending')) {
          setTxStatus('pending');
          setTxStatusError(null);
          scheduleRetry();
          return;
        }

        setTxStatus('error');
        setTxStatusError(payload?.tx_status ? `Unexpected tx status: ${payload.tx_status}` : 'Unknown tx status.');
      } catch (err) {
        if (!mounted) return;
        transientErrors += 1;
        if (transientErrors >= TX_POLL_MAX_TRANSIENT_ERRORS) {
          setTxStatus('error');
          setTxStatusError(
            err instanceof Error ? err.message : 'Failed to poll transaction status.',
          );
          return;
        }
        setTxStatus('pending');
        setTxStatusError(null);
        scheduleRetry();
      }
    };

    timer = setTimeout(() => {
      void poll();
    }, TX_POLL_INITIAL_DELAY_MS);
    return () => {
      mounted = false;
      if (timer) clearTimeout(timer);
    };
  }, [isReadOnly, txid]);

  const explorerUrl = txid
    ? `${scaffoldConfig.explorerBaseUrl}${txid}${scaffoldConfig.explorerChainQuery}`
    : null;

  return { data, loading, error, txid, txStatus, txStatusError, explorerUrl, call };
}
{% endif %}
{% endfor %}
{% endfor %}