feat: added contract interactivity
96114f26
7 file(s) · +169 −18
| 28 | 28 | "server": "workspace:*", |
|
| 29 | 29 | "shared": "workspace:*", |
|
| 30 | 30 | "tailwindcss": "^4.1.10", |
|
| 31 | - | "viem": "^2.21.1", |
|
| 31 | + | "viem": "^2.36.0", |
|
| 32 | 32 | }, |
|
| 33 | 33 | "devDependencies": { |
|
| 34 | 34 | "@eslint/js": "^9.28.0", |
|
| 773 | 773 | ||
| 774 | 774 | "client/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], |
|
| 775 | 775 | ||
| 776 | + | "client/viem": ["viem@2.36.0", "", { "dependencies": { "@noble/curves": "1.9.6", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.0.8", "isows": "1.0.7", "ox": "0.9.1", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-Xz7AkGtR43K+NY74X2lBevwfRrsXuifGUzt8QiULO47NXIcT7g3jcA4nIvl5m2OTE5v8SlzishwXmg64xOIVmQ=="], |
|
| 777 | + | ||
| 778 | + | "contracts/viem": ["viem@2.36.0", "", { "dependencies": { "@noble/curves": "1.9.6", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.0.8", "isows": "1.0.7", "ox": "0.9.1", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-Xz7AkGtR43K+NY74X2lBevwfRrsXuifGUzt8QiULO47NXIcT7g3jcA4nIvl5m2OTE5v8SlzishwXmg64xOIVmQ=="], |
|
| 779 | + | ||
| 776 | 780 | "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], |
|
| 777 | 781 | ||
| 778 | 782 | "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], |
|
| 780 | 784 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], |
|
| 781 | 785 | ||
| 782 | 786 | "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], |
|
| 787 | + | ||
| 788 | + | "server/viem": ["viem@2.36.0", "", { "dependencies": { "@noble/curves": "1.9.6", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.0.8", "isows": "1.0.7", "ox": "0.9.1", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-Xz7AkGtR43K+NY74X2lBevwfRrsXuifGUzt8QiULO47NXIcT7g3jcA4nIvl5m2OTE5v8SlzishwXmg64xOIVmQ=="], |
|
| 783 | 789 | ||
| 784 | 790 | "tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], |
|
| 785 | 791 | ||
| 796 | 802 | "@babel/helper-module-imports/@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], |
|
| 797 | 803 | ||
| 798 | 804 | "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], |
|
| 805 | + | ||
| 806 | + | "client/viem/@noble/curves": ["@noble/curves@1.9.6", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA=="], |
|
| 807 | + | ||
| 808 | + | "client/viem/ox": ["ox@0.9.1", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.8", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-NVI0cajROntJWtFnxZQ1aXDVy+c6DLEXJ3wwON48CgbPhmMJrpRTfVbuppR+47RmXm3lZ/uMaKiFSkLdAO1now=="], |
|
| 809 | + | ||
| 810 | + | "client/viem/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], |
|
| 811 | + | ||
| 812 | + | "contracts/viem/@noble/curves": ["@noble/curves@1.9.6", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA=="], |
|
| 813 | + | ||
| 814 | + | "contracts/viem/ox": ["ox@0.9.1", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.8", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-NVI0cajROntJWtFnxZQ1aXDVy+c6DLEXJ3wwON48CgbPhmMJrpRTfVbuppR+47RmXm3lZ/uMaKiFSkLdAO1now=="], |
|
| 815 | + | ||
| 816 | + | "contracts/viem/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], |
|
| 817 | + | ||
| 818 | + | "server/viem/@noble/curves": ["@noble/curves@1.9.6", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA=="], |
|
| 819 | + | ||
| 820 | + | "server/viem/ox": ["ox@0.9.1", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.8", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-NVI0cajROntJWtFnxZQ1aXDVy+c6DLEXJ3wwON48CgbPhmMJrpRTfVbuppR+47RmXm3lZ/uMaKiFSkLdAO1now=="], |
|
| 821 | + | ||
| 822 | + | "server/viem/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], |
|
| 799 | 823 | ||
| 800 | 824 | "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ=="], |
|
| 801 | 825 | ||
| 1 | + | { |
|
| 2 | + | "siteId": "fc1a09d0-a33a-4205-b746-9797226cdbc0", |
|
| 3 | + | "domain": "natspec-ui", |
|
| 4 | + | "buildCommand": "bun run build", |
|
| 5 | + | "buildDir": "dist" |
|
| 6 | + | } |
| 20 | 20 | "server": "workspace:*", |
|
| 21 | 21 | "shared": "workspace:*", |
|
| 22 | 22 | "tailwindcss": "^4.1.10", |
|
| 23 | - | "viem": "^2.21.1" |
|
| 23 | + | "viem": "^2.36.0" |
|
| 24 | 24 | }, |
|
| 25 | 25 | "devDependencies": { |
|
| 26 | 26 | "@eslint/js": "^9.28.0", |
| 2 | 2 | import { MarkdownUI } from '@markdown-ui/react'; |
|
| 3 | 3 | import { Marked } from 'marked'; |
|
| 4 | 4 | import { markedUiExtension } from '@markdown-ui/marked-ext'; |
|
| 5 | + | import { createPublicClient, createWalletClient, custom, http } from "viem"; |
|
| 6 | + | import { sepolia } from "viem/chains"; |
|
| 7 | + | import { parseContractToMarkdown, type ContractResponse } from './utils/contractParser'; |
|
| 5 | 8 | import '@markdown-ui/react/widgets.css'; |
|
| 6 | - | import { parseContractToMarkdown, type ContractResponse } from './utils/contractParser'; |
|
| 9 | + | ||
| 7 | 10 | ||
| 8 | 11 | const marked = new Marked().use(markedUiExtension); |
|
| 9 | 12 | ||
| 14 | 17 | const [contractHtml, setContractHtml] = useState<string>(""); |
|
| 15 | 18 | const [loading, setLoading] = useState<boolean>(true); |
|
| 16 | 19 | const [error, setError] = useState<string | null>(null); |
|
| 20 | + | const [contractData, setContractData] = useState<ContractResponse | null>(null); |
|
| 21 | + | const [counterValue, setCounterValue] = useState<bigint | undefined>(); |
|
| 22 | + | ||
| 23 | + | const publicClient = createPublicClient({ |
|
| 24 | + | chain: sepolia, |
|
| 25 | + | transport: http(), |
|
| 26 | + | }); |
|
| 27 | + | ||
| 28 | + | ||
| 29 | + | const walletClient = typeof window !== 'undefined' && (window as any).ethereum |
|
| 30 | + | ? createWalletClient({ |
|
| 31 | + | chain: sepolia, |
|
| 32 | + | transport: custom((window as any).ethereum), |
|
| 33 | + | }) |
|
| 34 | + | : null; |
|
| 35 | + | ||
| 36 | + | const handleWidgetEvent = async (event: { detail: { id: string, value: unknown } }) => { |
|
| 37 | + | if (!contractData || !walletClient) return; |
|
| 38 | + | ||
| 39 | + | console.log('Widget event received:', event.detail); |
|
| 40 | + | ||
| 41 | + | try { |
|
| 42 | + | const { id, value } = event.detail; |
|
| 43 | + | ||
| 44 | + | if (id === 'setNumber') { |
|
| 45 | + | const newValue = BigInt((value as any)?.newValue || (value as string) || 0); |
|
| 46 | + | console.log('Setting number to:', newValue); |
|
| 47 | + | ||
| 48 | + | const [account] = await walletClient.requestAddresses(); |
|
| 49 | + | ||
| 50 | + | const { request } = await publicClient.simulateContract({ |
|
| 51 | + | address: CONTRACT_ADDRESS as `0x${string}`, |
|
| 52 | + | abi: contractData.abi, |
|
| 53 | + | functionName: 'setNumber', |
|
| 54 | + | args: [newValue], |
|
| 55 | + | account, |
|
| 56 | + | }); |
|
| 57 | + | ||
| 58 | + | const hash = await walletClient.writeContract(request); |
|
| 59 | + | console.log('Transaction hash:', hash); |
|
| 60 | + | await readContractValue(); |
|
| 61 | + | } else if (id === 'increment') { |
|
| 62 | + | console.log('Incrementing counter'); |
|
| 63 | + | ||
| 64 | + | const [account] = await walletClient.requestAddresses(); |
|
| 65 | + | ||
| 66 | + | const { request } = await publicClient.simulateContract({ |
|
| 67 | + | address: CONTRACT_ADDRESS as `0x${string}`, |
|
| 68 | + | abi: contractData.abi, |
|
| 69 | + | functionName: 'increment', |
|
| 70 | + | args: [], |
|
| 71 | + | account, |
|
| 72 | + | }); |
|
| 73 | + | ||
| 74 | + | const hash = await walletClient.writeContract(request); |
|
| 75 | + | console.log('Transaction hash:', hash); |
|
| 76 | + | await readContractValue(); |
|
| 77 | + | } |
|
| 78 | + | } catch (error) { |
|
| 79 | + | console.error('Contract interaction error:', error); |
|
| 80 | + | } |
|
| 81 | + | }; |
|
| 82 | + | ||
| 83 | + | const readContractValue = async () => { |
|
| 84 | + | if (!contractData) return; |
|
| 85 | + | ||
| 86 | + | try { |
|
| 87 | + | const result = await publicClient.readContract({ |
|
| 88 | + | address: CONTRACT_ADDRESS as `0x${string}`, |
|
| 89 | + | abi: contractData.abi, |
|
| 90 | + | functionName: 'number', |
|
| 91 | + | args: [], |
|
| 92 | + | }); |
|
| 93 | + | setCounterValue(result as unknown as bigint); |
|
| 94 | + | } catch (error) { |
|
| 95 | + | console.error('Contract read error:', error); |
|
| 96 | + | } |
|
| 97 | + | }; |
|
| 17 | 98 | ||
| 18 | 99 | useEffect(() => { |
|
| 19 | 100 | const fetchContractData = async () => { |
|
| 20 | 101 | try { |
|
| 21 | 102 | setLoading(true); |
|
| 22 | 103 | const response = await fetch( |
|
| 23 | - | `https://sourcify.dev/server/v2/contract/${CHAIN_ID}/${CONTRACT_ADDRESS}?fields=devdoc` |
|
| 104 | + | `https://sourcify.dev/server/v2/contract/${CHAIN_ID}/${CONTRACT_ADDRESS}?fields=devdoc,abi` |
|
| 24 | 105 | ); |
|
| 25 | - | ||
| 106 | + | ||
| 26 | 107 | if (!response.ok) { |
|
| 27 | 108 | throw new Error(`HTTP error! status: ${response.status}`); |
|
| 28 | 109 | } |
|
| 29 | - | ||
| 110 | + | ||
| 30 | 111 | const data: ContractResponse = await response.json(); |
|
| 31 | - | ||
| 112 | + | setContractData(data); |
|
| 113 | + | ||
| 32 | 114 | const markdownContent = parseContractToMarkdown(data); |
|
| 33 | - | ||
| 115 | + | ||
| 34 | 116 | console.log(markdownContent); |
|
| 35 | 117 | const html = await marked.parse(markdownContent || '# No markdown widgets found'); |
|
| 36 | 118 | setContractHtml(html); |
|
| 45 | 127 | fetchContractData(); |
|
| 46 | 128 | }, []); |
|
| 47 | 129 | ||
| 130 | + | useEffect(() => { |
|
| 131 | + | if (contractData) { |
|
| 132 | + | readContractValue(); |
|
| 133 | + | } |
|
| 134 | + | }, [contractData]); |
|
| 135 | + | ||
| 136 | + | useEffect(() => { |
|
| 137 | + | if (!contractHtml) return; |
|
| 138 | + | ||
| 139 | + | // Set up click handlers for widget buttons |
|
| 140 | + | const timer = setTimeout(() => { |
|
| 141 | + | const buttons = document.querySelectorAll('button'); |
|
| 142 | + | ||
| 143 | + | buttons.forEach(button => { |
|
| 144 | + | const container = button.closest('.widget-container'); |
|
| 145 | + | if (container) { |
|
| 146 | + | const widget = container.querySelector('markdown-ui-widget'); |
|
| 147 | + | const widgetId = widget?.getAttribute('id'); |
|
| 148 | + | ||
| 149 | + | if (widgetId === 'increment' || widgetId === 'setNumber') { |
|
| 150 | + | button.addEventListener('click', async (e) => { |
|
| 151 | + | e.preventDefault(); |
|
| 152 | + | ||
| 153 | + | if (widgetId === 'increment') { |
|
| 154 | + | await handleWidgetEvent({ detail: { id: 'increment', value: null } }); |
|
| 155 | + | } else if (widgetId === 'setNumber') { |
|
| 156 | + | const input = container.querySelector('input') as HTMLInputElement; |
|
| 157 | + | const value = input ? input.value : '42'; |
|
| 158 | + | await handleWidgetEvent({ detail: { id: 'setNumber', value: { newValue: value } } }); |
|
| 159 | + | } |
|
| 160 | + | }); |
|
| 161 | + | } |
|
| 162 | + | } |
|
| 163 | + | }); |
|
| 164 | + | }, 1000); |
|
| 165 | + | ||
| 166 | + | return () => { |
|
| 167 | + | clearTimeout(timer); |
|
| 168 | + | }; |
|
| 169 | + | }, [contractHtml, contractData, walletClient]); |
|
| 170 | + | ||
| 171 | + | ||
| 48 | 172 | if (loading) { |
|
| 49 | 173 | return ( |
|
| 50 | 174 | <div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen"> |
|
| 63 | 187 | ||
| 64 | 188 | return ( |
|
| 65 | 189 | <div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen"> |
|
| 190 | + | {counterValue !== undefined && ( |
|
| 191 | + | <div className="text-center mb-4"> |
|
| 192 | + | <p className="text-lg font-semibold">Current Counter: {counterValue.toString()}</p> |
|
| 193 | + | </div> |
|
| 194 | + | )} |
|
| 66 | 195 | <MarkdownUI html={contractHtml} /> |
|
| 67 | 196 | </div> |
|
| 68 | 197 | ); |
|
| 14 | 14 | ||
| 15 | 15 | interface ContractResponse { |
|
| 16 | 16 | devdoc: DevDoc; |
|
| 17 | + | abi: any[]; |
|
| 17 | 18 | matchId?: string; |
|
| 18 | 19 | creationMatch?: string; |
|
| 19 | 20 | runtimeMatch?: string; |
| 1 | - | /// <reference types="vite/client" /> |
| 1 | 1 | import { defineConfig } from 'vite' |
|
| 2 | 2 | import react from '@vitejs/plugin-react' |
|
| 3 | 3 | import tailwindcss from '@tailwindcss/vite' |
|
| 4 | - | import path from 'path' |
|
| 5 | 4 | ||
| 6 | 5 | export default defineConfig({ |
|
| 7 | - | plugins: [react(), tailwindcss()], |
|
| 8 | - | resolve: { |
|
| 9 | - | alias: { |
|
| 10 | - | "@client": path.resolve(__dirname, "./src"), |
|
| 11 | - | "@server": path.resolve(__dirname, "../server/src"), |
|
| 12 | - | "@shared": path.resolve(__dirname, "../shared/src") |
|
| 13 | - | } |
|
| 14 | - | } |
|
| 6 | + | plugins: [react(), tailwindcss()] |
|
| 15 | 7 | }) |