stacksdapp-codegen 0.1.1

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 } from 'react';
import { Cl } from '@stacks/transactions';
import Message from '@/components/Message';
{% for contract in contracts %}
{% for fn in contract.functions %}
{% if fn.access == "public" or fn.access == "read_only" %}
import { use{{ contract.contract_name | upper_camel }}_{{ fn.name | upper_camel }} } from './hooks';
{% endif %}
{% endfor %}
{% endfor %}

{% raw %}
const S = {
  page:        { padding: '24px', display: 'flex', flexDirection: 'column' as const, gap: '24px', alignItems: 'center'},
  header:      { display: 'flex', alignItems: 'center', gap: '25px', paddingBottom: '16px', width: '428px' },
  headerDot:   { width: '8px', height: '8px', borderRadius: '50%', background: '#34d399' },
  headerTitle: { fontSize: '12px', fontWeight: 500, margin: 0, fontFamily: 'JetBrains Mono'  },
 tab: { 
    fontSize: '12px', 
    margin: 0, 
    fontFamily: 'JetBrains Mono', 
    background: 'transparent', 
    border: '1px solid transparent', 
    padding: 0, 
    cursor: 'pointer', 
    transition: 'all 0.15s ease',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center'
  },
  tabActive: { 
    background: '#434242', 
    width: '135px', 
    height: '40px', 
    borderRadius: '25px', 
    border: '1px solid #1F1E1F', 
    color: '#F4F3EF',
    lineHeight: '1',
    fontWeight: 400 // font-mono usually maps to normal weight unless specified
  },
  tabIdle: { 
    color: '#908E8E',
    width: '135px', // Keep width consistent so tabs don't jump when switching
    height: '40px'
  },
  tabHover: {
    color: '#FFFFFF'
  },
  headerTag:   { fontSize: '12px', color: '#6b7280', fontFamily: 'monospace', marginLeft: 'auto' },
 panel:       { borderRadius: '12px', border: '1px solid #1f2937', overflow: 'hidden', width: '428px', marginBottom: '12px' },
  panelBtn:    { width: '100%', textAlign: 'left' as const, padding: '16px 20px', background: '#1F1F1F', display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', border: 'none' },
  panelBtnL:   { display: 'flex', alignItems: 'center' },
  panelBar:    { width: '6px', height: '20px', borderRadius: '3px', background: '#FF550E', marginRight: '12px' },
  panelName:   { fontWeight: 500, color: '#F4F3EF', fontSize: '14px', fontFamily: 'Instrument Sans'  },
  panelExt:    { fontSize: '12px', color: '#374151', fontFamily: 'monospace' },
  panelGrid:   { padding: '20px', width:'100%' },
  resLabel:    { fontSize: '11px', color: '#9ca3af', fontWeight: 600, textTransform: 'uppercase' as const, letterSpacing: '0.05em', marginBottom: '4px' },
  resPre:      { fontSize: '12px', color: '#F4F3EF', overflowX: 'auto' as const, whiteSpace: 'pre-wrap' as const, wordBreak: 'break-word' as const, margin: 0 },
  txBox:       { borderRadius: '8px', background: 'rgba(6,78,59,0.2)', border: '1px solid #2E2D2E', padding: '8px 12px' },
  txLabel:     { fontSize: '11px', color: '#34d399', fontWeight: 600, marginBottom: '4px' },
  txHash:      { fontSize: '12px', color: '#F4F3EF', fontFamily: 'monospace', wordBreak: 'break-all' as const, margin: 0 },
  fnNumber:    { fontFamily: 'JetBrains Mono', fontSize: '12px', color: '#4B4B4B', marginRight: '12px' },
  card:        { borderRadius: '12px', border: '1px solid #1F1E1F', background: '#1A1B1E', padding: '24px', display: 'flex', flexDirection: 'column' as const, transition: 'all 0.3s ease' },
  formContainer: { overflow: 'hidden', transition: 'max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease' },
  cardHead: { 
    display: 'flex', 
    alignItems: 'center', 
    justifyContent: 'space-between',
    marginBottom: '4px'
  },
  fnName: { 
    fontFamily: 'Instrument Sans',
    fontSize: '12px', 
    fontWeight: 500, 
    color: '#908E8E' 
  },
  badgeRead: { 
    fontSize: '10px', 
    padding: '4px 10px', 
    borderRadius: '4px', 
    fontFamily: 'JetBrains Mono', 
    color: '#908E8E',
    display: 'flex',
    alignItems: 'center',
    gap: '4px'
  },
  badgeWrite: { 
    fontSize: '10px', 
    padding: '4px 10px', 
    borderRadius: '4px', 
    fontFamily: 'JetBrains Mono',  
    color: '#908E8E',
    display: 'flex',
    alignItems: 'center',
    gap: '4px'
  },
  form: { display: 'flex', flexDirection: 'column' as const, gap: '16px' },
  argLabel: { 
    fontSize: '11px', 
    color: '#908E8E', 
    fontFamily: 'JetBrains Mono', 
    marginBottom: '8px',
    display: 'block' 
  },
  argType: { 
    fontSize: '11px', 
    color: '#4B4B4B', 
    marginLeft: '8px' 
  },
  argInput: { 
    width: '100%', 
    borderRadius: '8px', 
    background: '#131416', // Deep dark input
    border: '1px solid #2C2C2C', 
    padding: '12px 16px', 
    fontSize: '14px', 
    fontFamily: 'JetBrains Mono', 
    color: '#FFFFFF', 
    outline: 'none', 
    boxSizing: 'border-box' as const 
  },
  btnRead: { 
    width: '100%', 
    padding: '12px', 
    borderRadius: '8px', 
    fontSize: '14px', 
    fontWeight: 600, 
    border: 'none', 
    background: '#FF550E',
    color: '#fff', 
    fontFamily: 'Instrument Sans',
    marginTop: '8px'
  },
  btnWrite: { 
    width: '100%', 
    padding: '12px', 
    borderRadius: '8px', 
    fontSize: '14px', 
    fontWeight: 600, 
    border: 'none', 
    background: '#FF550E',
    color: '#fff', 
    fontFamily: 'Instrument Sans',
    marginTop: '8px'
  },
  resBox: { 
    marginTop: '12px',
    borderRadius: '8px', 
    background: '#131416', 
    border: '1px solid #3B82F6', // Outline matches the action type
    padding: '12px' 
  },
  badgeDot: { 
    width: '6px', 
    height: '6px', 
    borderRadius: '50%', 
    background: 'currentColor', 
    marginRight: '6px' 
  },
} as const;

// Convert a raw string value to the appropriate ClarityValue based on the type hint
function toClarityValue(value: string, typeHint: string): import('@stacks/transactions').ClarityValue {
  const v = value.trim();
  const t = typeHint.toLowerCase();
  if (t === 'bool')    return Cl.bool(v === 'true' || v === '1');
  if (t.startsWith('uint'))  return Cl.uint(BigInt(v));
  if (t.startsWith('int'))   return Cl.int(BigInt(v));
  if (t === 'principal')     return Cl.principal(v);
  if (t.startsWith('string-utf8'))  return Cl.stringUtf8(v);
  if (t.startsWith('string-ascii')) return Cl.stringAscii(v);
  if (t.startsWith('buff'))  return Cl.bufferFromHex(v);
  // fallback: try uint, then string
  if (/^\d+$/.test(v)) return Cl.uint(BigInt(v));
  return Cl.stringAscii(v);
}
{% endraw %}

export default function DebugContracts() {
  const [activeTab, setActiveTab] = useState<'write' | 'read'>('write');
  const [hoveredTab, setHoveredTab] = useState<'write' | 'read' | null>(null);
  // Initialize with the first contract name to drop it down on load
  const [openContract, setOpenContract] = useState<string | null>("{{ contracts[0].contract_name }}");
  const [openFunction, setOpenFunction] = useState<string | null>(null);

  return (
    <div style={S.page}>
      <div style={S.header}>
        <button type="button" onMouseEnter={() => setHoveredTab('write')}
          onMouseLeave={() => setHoveredTab(null)} style={Object.assign({}, S.tab, activeTab === 'write' ? S.tabActive : S.tabIdle, (hoveredTab === 'write' && activeTab !== 'write') ? S.tabHover : {})} onClick={() => setActiveTab('write')}>
          Write Contracts
        </button>
        <button onMouseEnter={() => setHoveredTab('read')}
          onMouseLeave={() => setHoveredTab(null)} type="button" style={Object.assign({}, S.tab, activeTab === 'read' ? S.tabActive : S.tabIdle, (hoveredTab === 'read' && activeTab !== 'read') ? S.tabHover : {})} onClick={() => setActiveTab('read')}>
          Read Contracts
        </button>
      </div>

      {% for contract in contracts %}
      <div style={S.panel}>
        <button 
          style={S.panelBtn} 
          onClick={() => {
            setOpenContract(openContract === "{{ contract.contract_name }}" ? null : "{{ contract.contract_name }}");
            setOpenFunction(null); // Close functions when switching contracts
          }}
        >
          <div style={Object.assign({ display: 'flex', alignItems: 'center' })}>
            <div style={S.panelBar} />
            <span style={S.panelName}>{{ contract.contract_name | capitalize }}</span>
            <span style={S.panelExt}>.clar</span>
          </div>
          <span style={S.headerTag}>{openContract === "{{ contract.contract_name }}" ? '▲' : '▼'}</span>
        </button>

        {openContract === "{{ contract.contract_name }}" && (
          <div style={Object.assign({ padding: '20px', width:'100%' })} className='space-y-4'>
            {% for fn in contract.functions %}
            {% if fn.access == "public" or fn.access == "read_only" %}
              {% set fn_id = contract.contract_name ~ "_" ~ fn.name %}
              {% set current_idx = loop.index0 %}
{# Manual padding logic #}
{% if current_idx < 10 %}
  {% set padded_idx = "0" ~ current_idx %}
{% else %}
  {% set padded_idx = current_idx %}
{% endif %}
              
              {% if fn.access == "read_only" %}
  {activeTab === 'read' && (
    <FunctionCard_{{ contract.contract_name | upper_camel }}_{{ fn.name | upper_camel }} 
      num="{{ padded_idx }}"
      isOpen={openFunction === "{{ fn_id }}"}
      onToggle={() => setOpenFunction(openFunction === "{{ fn_id }}" ? null : "{{ fn_id }}")}
    />
  )}
              {% else %}
                {activeTab === 'write' && (
                  <FunctionCard_{{ contract.contract_name | upper_camel }}_{{ fn.name | upper_camel }} 
                    num="{{ padded_idx }}"
                    isOpen={openFunction === "{{ fn_id }}"}
                    onToggle={() => setOpenFunction(openFunction === "{{ fn_id }}" ? null : "{{ fn_id }}")}
                  />
                )}
              {% endif %}
            {% endif %}
            {% endfor %}
          </div>
        )}
      </div>
      {% endfor %}
      <Message />
    </div>
  );
}

function ContractPanel({ name, children }: { name: string; children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
  return (
    <div style={S.panel}>
      <button style={S.panelBtn} onClick={() => setOpen(o => !o)}>
        <div style={S.panelBtnL}>
          <div style={S.panelBar} />
          <span style={S.panelName}>{name}</span>
          <span style={S.panelExt}>.clar</span>
        </div>
        <span style={S.headerTag}>{open ? '▲' : '▼'}</span>
      </button>
      {open && <div style={S.panelGrid} className='space-y-4'>{children}</div>}
    </div>
  );
}

{% for contract in contracts %}
{% for fn in contract.functions %}
{% if fn.access == "public" or fn.access == "read_only" %}
function FunctionCard_{{ contract.contract_name | upper_camel }}_{{ fn.name | upper_camel }}({ num, isOpen, onToggle }: { num: string, isOpen: boolean, onToggle: () => void }) {
  const { data, loading, call } = use{{ contract.contract_name | upper_camel }}_{{ fn.name | upper_camel }}();
  const isReadOnly = {% if fn.access == "read_only" %}true{% else %}false{% endif %};

  {% if fn.args %}
  const [args, setArgs] = useState<Record<string, string>>({
    {% for arg in fn.args %}'{{ arg.name }}': '', {% endfor %}
  });
  const setArg = (name: string, value: string) => setArgs(a => Object.assign({}, a, { [name]: value }));
  {% endif %}

  return (
    <div style={Object.assign({}, S.card, { border: isOpen ? '1px solid #FF550E' : S.card.border })}>
      <div style={Object.assign({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer' })} onClick={onToggle}>
        <div style={Object.assign({ display: 'flex', alignItems: 'center' })}>
          <span style={S.fnNumber}>{num}</span>
          <span style={S.fnName}>{{ fn.name | upper }}</span>
        </div>
        <div style={isReadOnly ? S.badgeRead : S.badgeWrite}>
          <div style={S.badgeDot} />
          {isReadOnly ? 'READ' : 'WRITE'}
          <span style={Object.assign({ marginLeft: '8px', transition: '0.3s' }, { transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' })}>▼</span>
        </div>
      </div>

      <div style={Object.assign({}, S.formContainer, { maxHeight: isOpen ? '1000px' : '0px', opacity: isOpen ? 1 : 0, marginTop: isOpen ? '20px' : '0px' })}>
        <form style={S.form} onSubmit={async (e) => { e.preventDefault(); await call([{% if fn.args %}{% for arg in fn.args %}toClarityValue(args['{{ arg.name }}'] ?? '', '{{ arg.type_str }}'),{% endfor %}{% endif %}]); }}>
          {% if fn.args %}
            {% for arg in fn.args %}
            <div>
              <label style={S.argLabel}>{{ arg.name }} <span style={S.argType}>{{ arg.type_str | upper }}</span></label>
              <input style={S.argInput} value={args['{{ arg.name }}']} onChange={e => setArg('{{ arg.name }}', e.target.value)} />
            </div>
            {% endfor %}
          {% endif %}
          <button type="submit" disabled={loading} style={Object.assign({}, isReadOnly ? S.btnRead : S.btnWrite, { opacity: loading ? 0.5 : 1 })}>
            {loading ? 'PROCESSING...' : (isReadOnly ? 'READ' : 'CALL')}
          </button>
          {!!data && (
            <div style={Object.assign({}, S.resBox, { borderColor: isReadOnly ? '#2E2D2E' : '#2E2D2E' })}>
              <p style={S.resLabel}>RESULT</p>
              <pre style={S.resPre}>{JSON.stringify(data, null, 2)}</pre>
            </div>
          )}
        </form>
      </div>
    </div>
  );
}
{% endif %}
{% endfor %}
{% endfor %}