client/src/App.tsx 6.6 K raw
1
import { useState, useEffect, useCallback } from "react";
2
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
import { WagmiProvider, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
4
import { MarkdownUI } from '@markdown-ui/react';
5
import { Marked } from 'marked';
6
import { markedUiExtension } from '@markdown-ui/marked-ext';
7
import { config } from './wagmi-config';
8
import { parseContractToMarkdown, type ContractResponse } from './utils/contractParser';
9
import './App.css';
10
11
const queryClient = new QueryClient();
12
13
function App() {
14
  return (
15
    <WagmiProvider config={config}>
16
      <QueryClientProvider client={queryClient}>
17
        <ContractApp />
18
      </QueryClientProvider>
19
    </WagmiProvider>
20
  );
21
}
22
23
export default App;
24
25
const marked = new Marked().use(markedUiExtension);
26
27
const CONTRACT_ADDRESS = "0xEeF9B4a84C3327860CD14E1E066D7D6762b9bC3F" as `0x${string}`;
28
const CHAIN_ID = "11155111"; // Sepolia
29
30
function ContractApp() {
31
  const [contractHtml, setContractHtml] = useState<string>("");
32
  const [loading, setLoading] = useState<boolean>(true);
33
  const [error, setError] = useState<string | null>(null);
34
  const [contractData, setContractData] = useState<ContractResponse | null>(null);
35
36
  // Use Wagmi hook to read the counter value
37
  const { data: counterValue, refetch: refetchCounter } = useReadContract({
38
    address: CONTRACT_ADDRESS,
39
    abi: contractData?.abi || [],
40
    functionName: 'number',
41
    args: [],
42
    query: {
43
      enabled: !!contractData?.abi,
44
    },
45
  });
46
47
  // Use Wagmi hooks for writing to contract
48
  const { data: hash, writeContract, isPending } = useWriteContract();
49
  const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({
50
    hash,
51
  });
52
53
  const handleWidgetEvent = useCallback(async (event: { detail: { id: string, value: unknown } }) => {
54
    if (!contractData) return;
55
56
    console.log('Widget event received:', event.detail);
57
58
    try {
59
      const { id, value } = event.detail;
60
61
      if (id === 'setNumber') {
62
        const newValue = BigInt((value as { newValue?: string })?.newValue || (value as string) || 0);
63
        console.log('Setting number to:', newValue);
64
65
        writeContract({
66
          address: CONTRACT_ADDRESS,
67
          abi: contractData.abi,
68
          functionName: 'setNumber',
69
          args: [newValue],
70
        });
71
      } else if (id === 'increment') {
72
        console.log('Incrementing counter');
73
74
        writeContract({
75
          address: CONTRACT_ADDRESS,
76
          abi: contractData.abi,
77
          functionName: 'increment',
78
          args: [],
79
        });
80
      }
81
    } catch (error) {
82
      console.error('Contract interaction error:', error);
83
    }
84
  }, [contractData, writeContract]);
85
86
  useEffect(() => {
87
    const fetchContractData = async () => {
88
      try {
89
        setLoading(true);
90
        const response = await fetch(
91
          `https://sourcify.dev/server/v2/contract/${CHAIN_ID}/${CONTRACT_ADDRESS}?fields=devdoc,abi`
92
        );
93
94
        if (!response.ok) {
95
          throw new Error(`HTTP error! status: ${response.status}`);
96
        }
97
98
        const data: ContractResponse = await response.json();
99
        setContractData(data);
100
101
        const markdownContent = parseContractToMarkdown(data);
102
103
        console.log(markdownContent);
104
        const html = await marked.parse(markdownContent || '# No markdown widgets found');
105
        setContractHtml(html);
106
      } catch (err) {
107
        console.error('Error fetching contract data:', err);
108
        setError(err instanceof Error ? err.message : 'Unknown error');
109
      } finally {
110
        setLoading(false);
111
      }
112
    };
113
114
    fetchContractData();
115
  }, []);
116
117
  // Refetch counter value when transaction is confirmed
118
  useEffect(() => {
119
    if (isConfirmed) {
120
      refetchCounter();
121
    }
122
  }, [isConfirmed, refetchCounter]);
123
124
  useEffect(() => {
125
    if (!contractHtml) return;
126
127
    // Set up click handlers for widget buttons
128
    const timer = setTimeout(() => {
129
      const buttons = document.querySelectorAll('button');
130
      
131
      buttons.forEach(button => {
132
        const container = button.closest('.widget-container');
133
        if (container) {
134
          const widget = container.querySelector('markdown-ui-widget');
135
          const widgetId = widget?.getAttribute('id');
136
          
137
          if (widgetId === 'increment' || widgetId === 'setNumber') {
138
            button.addEventListener('click', async (e) => {
139
              e.preventDefault();
140
              
141
              if (widgetId === 'increment') {
142
                await handleWidgetEvent({ detail: { id: 'increment', value: null } });
143
              } else if (widgetId === 'setNumber') {
144
                const input = container.querySelector('input') as HTMLInputElement;
145
                const value = input ? input.value : '42';
146
                await handleWidgetEvent({ detail: { id: 'setNumber', value: { newValue: value } } });
147
              }
148
            });
149
          }
150
        }
151
      });
152
    }, 1000);
153
154
    return () => {
155
      clearTimeout(timer);
156
    };
157
  }, [contractHtml, contractData, handleWidgetEvent]);
158
159
  if (loading) {
160
    return (
161
      <div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
162
        <p>Loading contract data...</p>
163
      </div>
164
    );
165
  }
166
167
  if (error) {
168
    return (
169
      <div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
170
        <p className="text-red-500">Error: {error}</p>
171
      </div>
172
    );
173
  }
174
175
  return (
176
    <div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
177
      {counterValue !== undefined && (
178
        <div className="text-center mb-4">
179
          <p className="text-lg font-semibold">Current Counter: {counterValue.toString()}</p>
180
        </div>
181
      )}
182
      {isPending && (
183
        <div className="text-center mb-4">
184
          <p className="text-blue-500">Transaction pending...</p>
185
        </div>
186
      )}
187
      {isConfirming && (
188
        <div className="text-center mb-4">
189
          <p className="text-yellow-500">Waiting for confirmation...</p>
190
        </div>
191
      )}
192
      {isConfirmed && hash && (
193
        <div className="text-center mb-4">
194
          <p className="text-green-500">Transaction confirmed!</p>
195
          <p className="text-sm text-gray-600 break-all">
196
            Hash: <a 
197
              href={`https://sepolia.etherscan.io/tx/${hash}`} 
198
              target="_blank" 
199
              rel="noopener noreferrer"
200
              className="text-blue-500 hover:text-blue-700 underline"
201
            >
202
              {hash}
203
            </a>
204
          </p>
205
        </div>
206
      )}
207
      <MarkdownUI html={contractHtml} />
208
    </div>
209
  );
210
}