feat: added contract-read 5846ad3d
Steve · 2025-10-18 16:39 3 file(s) · +771 −67
scripts/build-components.ts +79 −63
1 1
import { mkdir, copyFile, readFile, writeFile } from "node:fs/promises";
2 2
import { existsSync } from "node:fs";
3 -
import { join } from "node:path";
4 3
5 4
async function buildComponents() {
6 -
  console.log("🔨 Building components...");
5 +
	console.log("🔨 Building components...");
7 6
8 -
  // Ensure dist/components directory exists
9 -
  const distComponentsDir = "dist/components";
10 -
  if (!existsSync(distComponentsDir)) {
11 -
    await mkdir(distComponentsDir, { recursive: true });
12 -
  }
7 +
	// Ensure dist/components directory exists
8 +
	const distComponentsDir = "dist/components";
9 +
	if (!existsSync(distComponentsDir)) {
10 +
		await mkdir(distComponentsDir, { recursive: true });
11 +
	}
13 12
14 -
  // Copy connect-wallet.js as-is (no dependencies)
15 -
  await copyFile(
16 -
    "src/components/connect-wallet.js",
17 -
    "dist/components/connect-wallet.js"
18 -
  );
19 -
  console.log("✅ Copied connect-wallet.js");
13 +
	// Copy connect-wallet.js as-is (no dependencies)
14 +
	await copyFile(
15 +
		"src/components/connect-wallet.js",
16 +
		"dist/components/connect-wallet.js",
17 +
	);
18 +
	console.log("✅ Copied connect-wallet.js");
20 19
21 -
  // Bundle contract-call.js with its dependencies
22 -
  const result = await Bun.build({
23 -
    entrypoints: ["src/components/contract-call.js"],
24 -
    target: "browser",
25 -
    format: "esm",
26 -
    minify: false,
27 -
    sourcemap: "none",
28 -
    splitting: false,
29 -
    outdir: "dist/components",
30 -
    naming: "[dir]/[name].[ext]",
31 -
    external: [], // Bundle all dependencies
32 -
    plugins: [
33 -
        {
34 -
          name: "Keep JSDocs",
35 -
          setup(build) {
36 -
            build.onLoad({ filter: /\.(js)$/ }, async ({ path }) => {
37 -
              let text = await Bun.file(path).text();
38 -
              // Replace '/**' with '/*! *' to mark comments as "important" for minifiers
39 -
              let contents = text.replaceAll('/**', '/*! *');
40 -
              return { contents, loader: 'js' };
41 -
            });
42 -
          },
43 -
        },
44 -
      ],
45 -
  });
20 +
	// Helper function to bundle and reorganize component with dependencies
21 +
	async function bundleComponent(componentName: string, className: string) {
22 +
		const result = await Bun.build({
23 +
			entrypoints: [`src/components/${componentName}.js`],
24 +
			target: "browser",
25 +
			format: "esm",
26 +
			minify: false,
27 +
			sourcemap: "none",
28 +
			splitting: false,
29 +
			outdir: "dist/components",
30 +
			naming: "[dir]/[name].[ext]",
31 +
			external: [], // Bundle all dependencies
32 +
			plugins: [
33 +
				{
34 +
					name: "Keep JSDocs",
35 +
					setup(build) {
36 +
						build.onLoad({ filter: /\.(js)$/ }, async ({ path }) => {
37 +
							let text = await Bun.file(path).text();
38 +
							// Replace '/**' with '/*! *' to mark comments as "important" for minifiers
39 +
							let contents = text.replaceAll("/**", "/*! *");
40 +
							return { contents, loader: "js" };
41 +
						});
42 +
					},
43 +
				},
44 +
			],
45 +
		});
46 46
47 -
  if (!result.success) {
48 -
    console.error("❌ Build failed:");
49 -
    for (const log of result.logs) {
50 -
      console.error(log);
51 -
    }
52 -
    process.exit(1);
53 -
  }
47 +
		if (!result.success) {
48 +
			console.error(`❌ Build failed for ${componentName}:`);
49 +
			for (const log of result.logs) {
50 +
				console.error(log);
51 +
			}
52 +
			process.exit(1);
53 +
		}
54 54
55 -
  console.log("✅ Bundled contract-call.js with dependencies");
55 +
		console.log(`✅ Bundled ${componentName}.js with dependencies`);
56 56
57 -
  // Read the bundled file
58 -
  const bundledContent = await readFile("dist/components/contract-call.js", "utf8");
57 +
		// Read the bundled file
58 +
		const bundledContent = await readFile(
59 +
			`dist/components/${componentName}.js`,
60 +
			"utf8",
61 +
		);
59 62
60 -
  // Split content to move dependencies to bottom
61 -
  const lines = bundledContent.split('\n');
62 -
  const componentStartIndex = lines.findIndex(line =>
63 -
    line.includes('class ContractCall extends HTMLElement')
64 -
  );
63 +
		// Split content to move dependencies to bottom
64 +
		const lines = bundledContent.split("\n");
65 +
		const componentStartIndex = lines.findIndex((line) =>
66 +
			line.includes(`class ${className} extends HTMLElement`),
67 +
		);
65 68
66 -
  if (componentStartIndex > 0) {
67 -
    // Dependencies are at the top (before the component class)
68 -
    const dependencies = lines.slice(0, componentStartIndex).join('\n');
69 -
    const componentCode = lines.slice(componentStartIndex).join('\n');
69 +
		if (componentStartIndex > 0) {
70 +
			// Dependencies are at the top (before the component class)
71 +
			const dependencies = lines.slice(0, componentStartIndex).join("\n");
72 +
			const componentCode = lines.slice(componentStartIndex).join("\n");
70 73
71 -
    // Create new content with component first, then dependencies
72 -
    const newContent = `// User-editable contract call component
74 +
			// Create new content with component first, then dependencies
75 +
			const newContent = `// User-editable ${componentName} component
73 76
// @noble/hashes are bundled at the bottom of this file
74 77
75 78
${componentCode}
80 83
81 84
${dependencies}`;
82 85
83 -
    await writeFile("dist/components/contract-call.js", newContent, "utf8");
84 -
    console.log("✅ Reorganized contract-call.js (component code first, dependencies at bottom)");
85 -
  }
86 +
			await writeFile(
87 +
				`dist/components/${componentName}.js`,
88 +
				newContent,
89 +
				"utf8",
90 +
			);
91 +
			console.log(
92 +
				`✅ Reorganized ${componentName}.js (component code first, dependencies at bottom)`,
93 +
			);
94 +
		}
95 +
	}
96 +
97 +
	// Bundle contract-call.js with its dependencies
98 +
	await bundleComponent("contract-call", "ContractCall");
86 99
87 -
  console.log("🎉 Component build complete!");
100 +
	// Bundle contract-read.js with its dependencies
101 +
	await bundleComponent("contract-read", "ContractRead");
102 +
103 +
	console.log("🎉 Component build complete!");
88 104
}
89 105
90 106
buildComponents().catch(console.error);
site/index.html +14 −4
60 60
          abi='[{"inputs":[],"name":"increment","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"number","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"newNumber","type":"uint256"}],"name":"setNumber","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
61 61
          button-text="Increment"
62 62
        ></contract-call>
63 -
        <contract-call
63 +
      </div>
64 +
      <div class="text-xl text-[#ccc]">contract-call</div>
65 +
      <div class="bg-[#1e1e1e] border border-[#333] rounded-sm p-2 text-xs text-[#888] mt-2 text-center">
66 +
        npx norns-ui@latest add contract-call
67 +
      </div>
68 +
    </div>
69 +
    <div class="flex flex-col items-start gap-4">
70 +
      <div class="border border-white rounded-md sm:w-[400px] sm:h-[400px] h-[300px] w-[300px] p-4 flex flex-col gap-4 items-center justify-center">
71 +
        <contract-read
64 72
          contract-address="0x8C9EC9c13812C7F9F26AB934d4bF36206240dDA8"
65 73
          chain-id="11155111"
66 74
          method-name="number"
75 +
          rpc-url="https://sepolia.drpc.org"
67 76
          abi='[{"inputs":[],"name":"increment","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"number","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"newNumber","type":"uint256"}],"name":"setNumber","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
68 77
          button-text="Current Count"
69 -
        ></contract-call>
78 +
        ></contract-read>
70 79
      </div>
71 -
      <div class="text-xl text-[#ccc]">contract-call</div>
80 +
      <div class="text-xl text-[#ccc]">contract-read</div>
72 81
      <div class="bg-[#1e1e1e] border border-[#333] rounded-sm p-2 text-xs text-[#888] mt-2 text-center">
73 -
        npx norns-ui@latest add contract-call
82 +
        npx norns-ui@latest add contract-read
74 83
      </div>
75 84
    </div>
76 85
  </div>
78 87
79 88
  <script src="../src/components/connect-wallet.js"></script>
80 89
  <script src="../src/components/contract-call.js"></script>
90 +
  <script src="../src/components/contract-read.js"></script>
81 91
</body>
82 92
</html>
src/components/contract-read.js (added) +678 −0
1 +
import { keccak_256 } from "@noble/hashes/sha3.js";
2 +
3 +
class ContractRead extends HTMLElement {
4 +
	/**
5 +
	 * ContractRead Web Component
6 +
	 *
7 +
	 * A custom HTML element for reading state from Ethereum smart contracts.
8 +
	 * Automatically loads and displays read-only contract state on mount.
9 +
	 * Supports both wallet integration and direct RPC calls.
10 +
	 *
11 +
	 * @example
12 +
	 * Basic usage with inline ABI and RPC URL:
13 +
	 * ```html
14 +
	 * <contract-read
15 +
	 *   contract-address="0x1234567890123456789012345678901234567890"
16 +
	 *   method-name="balanceOf"
17 +
	 *   method-args='["0xabcdef1234567890123456789012345678901234"]'
18 +
	 *   rpc-url="https://eth.llamarpc.com"
19 +
	 *   abi='[{"type":"function","name":"balanceOf","inputs":[{"type":"address","name":"owner"}],"outputs":[{"type":"uint256","name":""}],"stateMutability":"view"}]'>
20 +
	 * </contract-read>
21 +
	 * ```
22 +
	 *
23 +
	 * @example
24 +
	 * Using ABI from URL with wallet:
25 +
	 * ```html
26 +
	 * <contract-read
27 +
	 *   contract-address="0x1234567890123456789012345678901234567890"
28 +
	 *   method-name="totalSupply"
29 +
	 *   abi-url="https://api.example.com/contract-abi.json">
30 +
	 * </contract-read>
31 +
	 * ```
32 +
	 *
33 +
	 * @example
34 +
	 * Custom styling:
35 +
	 * ```html
36 +
	 * <contract-read
37 +
	 *   contract-address="0x1234567890123456789012345678901234567890"
38 +
	 *   method-name="getName"
39 +
	 *   abi-url="/abi/token.json"
40 +
	 *   rpc-url="https://eth.llamarpc.com"
41 +
	 *   background="#1a1a1a"
42 +
	 *   foreground="#ffffff"
43 +
	 *   primary="#00ff88"
44 +
	 *   border-radius="8px"
45 +
	 *   error-color="#ff4444"
46 +
	 *   success-color="#44ff44">
47 +
	 * </contract-read>
48 +
	 * ```
49 +
	 *
50 +
	 * Attributes:
51 +
	 * - contract-address (required): The Ethereum contract address
52 +
	 * - method-name (required): The contract method to call
53 +
	 * - method-args: JSON array of method arguments (default: [])
54 +
	 * - abi: JSON string of the contract ABI
55 +
	 * - abi-url: URL to fetch the contract ABI from
56 +
	 * - rpc-url: RPC URL for direct calls (used when wallet is not available)
57 +
	 * - chain-id: Ethereum chain ID in hex format (default: "0x1" for mainnet)
58 +
	 * - background: Background color (default: "#232323")
59 +
	 * - foreground: Text color (default: "#ffffff")
60 +
	 * - primary: Primary color (default: "#5F8787")
61 +
	 * - border-radius: Border radius for UI elements (default: "4px")
62 +
	 * - error-color: Color for error messages (default: "#E78A53")
63 +
	 * - success-color: Color for success messages (default: "#5F8787")
64 +
	 *
65 +
	 * Events:
66 +
	 * - abi-loaded: Fired when ABI is successfully loaded from URL
67 +
	 * - abi-error: Fired when ABI loading fails
68 +
	 * - contract-read-success: Fired when contract read succeeds
69 +
	 * - contract-read-error: Fired when contract read fails
70 +
	 *
71 +
	 * Requirements:
72 +
	 * - @noble/hashes library for keccak256 hashing
73 +
	 * - Either a wallet extension (MetaMask) or rpc-url attribute
74 +
	 *
75 +
	 * Notes:
76 +
	 * - Only supports read-only methods (view/pure)
77 +
	 * - Automatically loads state on component mount
78 +
	 * - Uses wallet if available, falls back to RPC URL
79 +
	 * - Simplified ABI encoding/decoding (use ethers.js/web3.js for production)
80 +
	 */
81 +
82 +
	// Constructor and lifecycle methods
83 +
	constructor() {
84 +
		super();
85 +
		this.attachShadow({ mode: "open" });
86 +
		this.loading = false;
87 +
		this.result = null;
88 +
		this.error = null;
89 +
		this.abi = null;
90 +
		this.methodData = null;
91 +
		this.contractAddress = "";
92 +
		this.chainId = "0x1";
93 +
		this.methodName = "";
94 +
		this.methodArgs = [];
95 +
		this.abiUrl = "";
96 +
		this.rpcUrl = "";
97 +
		this.isReadOnly = true;
98 +
	}
99 +
100 +
	static get observedAttributes() {
101 +
		return [
102 +
			"contract-address",
103 +
			"chain-id",
104 +
			"method-name",
105 +
			"method-args",
106 +
			"abi-url",
107 +
			"abi",
108 +
			"rpc-url",
109 +
			"background",
110 +
			"foreground",
111 +
			"primary",
112 +
			"border-radius",
113 +
			"error-color",
114 +
			"success-color",
115 +
		];
116 +
	}
117 +
118 +
	attributeChangedCallback(name, oldValue, newValue) {
119 +
		if (oldValue === newValue) return;
120 +
121 +
		switch (name) {
122 +
			case "contract-address":
123 +
				this.contractAddress = newValue || "";
124 +
				break;
125 +
			case "chain-id":
126 +
				this.chainId = this.normalizeChainId(newValue);
127 +
				break;
128 +
			case "method-name":
129 +
				this.methodName = newValue || "";
130 +
				this.parseMethodFromAbi();
131 +
				break;
132 +
			case "method-args":
133 +
				try {
134 +
					this.methodArgs = newValue ? JSON.parse(newValue) : [];
135 +
				} catch (e) {
136 +
					console.error("Invalid method-args JSON:", e);
137 +
					this.methodArgs = [];
138 +
				}
139 +
				break;
140 +
			case "abi-url":
141 +
				this.abiUrl = newValue || "";
142 +
				if (this.abiUrl) {
143 +
					this.fetchAbi();
144 +
				}
145 +
				break;
146 +
			case "abi":
147 +
				try {
148 +
					this.abi = newValue ? JSON.parse(newValue) : null;
149 +
					this.parseMethodFromAbi();
150 +
				} catch (e) {
151 +
					console.error("Invalid ABI JSON:", e);
152 +
					this.abi = null;
153 +
				}
154 +
				break;
155 +
			case "rpc-url":
156 +
				this.rpcUrl = newValue || "";
157 +
				break;
158 +
			default:
159 +
				if (
160 +
					[
161 +
						"background",
162 +
						"foreground",
163 +
						"primary",
164 +
						"border-radius",
165 +
						"error-color",
166 +
						"success-color",
167 +
					].includes(name)
168 +
				) {
169 +
					this.render();
170 +
				}
171 +
		}
172 +
	}
173 +
174 +
	connectedCallback() {
175 +
		this.contractAddress = this.getAttribute("contract-address") || "";
176 +
		const chainIdAttr = this.getAttribute("chain-id");
177 +
		this.chainId = this.normalizeChainId(chainIdAttr);
178 +
		this.methodName = this.getAttribute("method-name") || "";
179 +
		this.abiUrl = this.getAttribute("abi-url") || "";
180 +
		this.rpcUrl = this.getAttribute("rpc-url") || "";
181 +
182 +
		try {
183 +
			const methodArgsAttr = this.getAttribute("method-args");
184 +
			this.methodArgs = methodArgsAttr ? JSON.parse(methodArgsAttr) : [];
185 +
		} catch (e) {
186 +
			console.error("Invalid method-args JSON:", e);
187 +
			this.methodArgs = [];
188 +
		}
189 +
190 +
		try {
191 +
			const abiAttr = this.getAttribute("abi");
192 +
			this.abi = abiAttr ? JSON.parse(abiAttr) : null;
193 +
		} catch (e) {
194 +
			console.error("Invalid ABI JSON:", e);
195 +
			this.abi = null;
196 +
		}
197 +
198 +
		if (this.abiUrl && !this.abi) {
199 +
			this.fetchAbi().then(() => {
200 +
				// Auto-call contract after ABI is loaded
201 +
				if (this.methodData) {
202 +
					this.readContract();
203 +
				}
204 +
			});
205 +
		} else if (this.abi) {
206 +
			this.parseMethodFromAbi();
207 +
			// Auto-call contract on mount
208 +
			if (this.methodData) {
209 +
				this.readContract();
210 +
			}
211 +
		}
212 +
213 +
		this.render();
214 +
	}
215 +
216 +
	// ABI and method parsing
217 +
	async fetchAbi() {
218 +
		if (!this.abiUrl) return;
219 +
220 +
		try {
221 +
			this.loading = true;
222 +
			this.render();
223 +
224 +
			const response = await fetch(this.abiUrl);
225 +
			if (!response.ok) {
226 +
				throw new Error(`Failed to fetch ABI: ${response.statusText}`);
227 +
			}
228 +
229 +
			this.abi = await response.json();
230 +
			this.parseMethodFromAbi();
231 +
			this.loading = false;
232 +
			this.render();
233 +
234 +
			this.dispatchEvent(
235 +
				new CustomEvent("abi-loaded", {
236 +
					detail: { abi: this.abi },
237 +
				}),
238 +
			);
239 +
		} catch (error) {
240 +
			console.error("Failed to fetch ABI:", error);
241 +
			this.error = error.message;
242 +
			this.loading = false;
243 +
			this.render();
244 +
245 +
			this.dispatchEvent(
246 +
				new CustomEvent("abi-error", {
247 +
					detail: { error: error.message },
248 +
				}),
249 +
			);
250 +
		}
251 +
	}
252 +
253 +
	parseMethodFromAbi() {
254 +
		if (!this.abi || !this.methodName) return;
255 +
256 +
		const method = this.abi.find(
257 +
			(item) => item.type === "function" && item.name === this.methodName,
258 +
		);
259 +
260 +
		if (!method) {
261 +
			this.error = `Method '${this.methodName}' not found in ABI`;
262 +
			this.render();
263 +
			return;
264 +
		}
265 +
266 +
		this.methodData = method;
267 +
		this.isReadOnly =
268 +
			method.stateMutability === "view" || method.stateMutability === "pure";
269 +
		this.error = null;
270 +
		this.render();
271 +
	}
272 +
273 +
	/**
274 +
	 * Converts a numeric chain ID to hex format
275 +
	 * @param {string|number} chainId - Chain ID in numeric or hex format
276 +
	 * @returns {string} Chain ID in hex format (e.g., "0x2105")
277 +
	 */
278 +
	normalizeChainId(chainId) {
279 +
		if (!chainId) return "0x1";
280 +
281 +
		const chainIdStr = String(chainId);
282 +
283 +
		// If it's already in hex format (starts with 0x), return as-is
284 +
		if (chainIdStr.startsWith("0x")) {
285 +
			return chainIdStr.toLowerCase();
286 +
		}
287 +
288 +
		// Convert numeric string to hex
289 +
		const numericChainId = parseInt(chainIdStr, 10);
290 +
		if (isNaN(numericChainId)) {
291 +
			console.warn(`Invalid chain ID: ${chainId}, defaulting to 0x1`);
292 +
			return "0x1";
293 +
		}
294 +
295 +
		return `0x${numericChainId.toString(16)}`;
296 +
	}
297 +
298 +
	// Contract interaction methods
299 +
	async readContract() {
300 +
		if (!this.contractAddress || !this.methodName || !this.methodData) {
301 +
			this.error = "Missing required contract information";
302 +
			this.render();
303 +
			return;
304 +
		}
305 +
306 +
		try {
307 +
			this.loading = true;
308 +
			this.result = null;
309 +
			this.error = null;
310 +
			this.render();
311 +
312 +
			// Encode method call
313 +
			const methodSignature = this.encodeMethodSignature();
314 +
			const encodedArgs = this.encodeArguments();
315 +
			const data = methodSignature + encodedArgs;
316 +
317 +
			let result;
318 +
319 +
			// Try to use wallet first, fall back to RPC URL
320 +
			if (window.ethereum) {
321 +
				try {
322 +
					result = await window.ethereum.request({
323 +
						method: "eth_call",
324 +
						params: [
325 +
							{
326 +
								to: this.contractAddress,
327 +
								data: data,
328 +
							},
329 +
							"latest",
330 +
						],
331 +
					});
332 +
				} catch (walletError) {
333 +
					console.warn("Wallet call failed, trying RPC URL:", walletError);
334 +
					if (this.rpcUrl) {
335 +
						result = await this.callViaRpc(data);
336 +
					} else {
337 +
						throw walletError;
338 +
					}
339 +
				}
340 +
			} else if (this.rpcUrl) {
341 +
				// No wallet available, use RPC URL
342 +
				result = await this.callViaRpc(data);
343 +
			} else {
344 +
				throw new Error(
345 +
					"No wallet extension found and no RPC URL provided. Please install MetaMask or provide an rpc-url attribute.",
346 +
				);
347 +
			}
348 +
349 +
			this.result = this.decodeResult(result);
350 +
			this.loading = false;
351 +
			this.render();
352 +
353 +
			this.dispatchEvent(
354 +
				new CustomEvent("contract-read-success", {
355 +
					detail: {
356 +
						result: this.result,
357 +
						method: this.methodName,
358 +
						args: this.methodArgs,
359 +
					},
360 +
				}),
361 +
			);
362 +
		} catch (error) {
363 +
			console.error("Contract read failed:", error);
364 +
			this.error = error.message;
365 +
			this.loading = false;
366 +
			this.render();
367 +
368 +
			this.dispatchEvent(
369 +
				new CustomEvent("contract-read-error", {
370 +
					detail: { error: error.message },
371 +
				}),
372 +
			);
373 +
		}
374 +
	}
375 +
376 +
	async callViaRpc(data) {
377 +
		if (!this.rpcUrl) {
378 +
			throw new Error("RPC URL not configured");
379 +
		}
380 +
381 +
		const response = await fetch(this.rpcUrl, {
382 +
			method: "POST",
383 +
			headers: {
384 +
				"Content-Type": "application/json",
385 +
			},
386 +
			body: JSON.stringify({
387 +
				jsonrpc: "2.0",
388 +
				method: "eth_call",
389 +
				params: [
390 +
					{
391 +
						to: this.contractAddress,
392 +
						data: data,
393 +
					},
394 +
					"latest",
395 +
				],
396 +
				id: 1,
397 +
			}),
398 +
		});
399 +
400 +
		if (!response.ok) {
401 +
			throw new Error(`RPC request failed: ${response.statusText}`);
402 +
		}
403 +
404 +
		const json = await response.json();
405 +
406 +
		if (json.error) {
407 +
			throw new Error(`RPC error: ${json.error.message}`);
408 +
		}
409 +
410 +
		return json.result;
411 +
	}
412 +
413 +
	// Encoding and decoding methods
414 +
	encodeMethodSignature() {
415 +
		const inputs = this.methodData.inputs || [];
416 +
		const types = inputs.map((input) => input.type).join(",");
417 +
		const signature = `${this.methodName}(${types})`;
418 +
419 +
		// Use proper keccak256 hash - for production use a crypto library
420 +
		const hash = this.keccak256(signature);
421 +
		return "0x" + hash.slice(0, 8); // First 4 bytes (function selector)
422 +
	}
423 +
424 +
	encodeArguments() {
425 +
		const inputs = this.methodData.inputs || [];
426 +
427 +
		// If method has no inputs or no args provided, return empty string
428 +
		if (!inputs.length || !this.methodArgs.length) return "";
429 +
430 +
		// This is a simplified encoding - in production, use ethers.js or web3.js
431 +
		let encoded = "";
432 +
433 +
		for (let i = 0; i < this.methodArgs.length; i++) {
434 +
			const arg = this.methodArgs[i];
435 +
			const input = inputs[i];
436 +
437 +
			if (!input) continue;
438 +
439 +
			encoded += this.encodeValue(arg, input.type);
440 +
		}
441 +
442 +
		return encoded;
443 +
	}
444 +
445 +
	encodeValue(value, type) {
446 +
		// Simplified encoding - use proper ABI encoding library in production
447 +
		if (type === "uint256" || type === "uint") {
448 +
			return BigInt(value).toString(16).padStart(64, "0");
449 +
		} else if (type === "address") {
450 +
			return value.toLowerCase().replace("0x", "").padStart(64, "0");
451 +
		} else if (type === "string") {
452 +
			const hex = Array.from(new TextEncoder().encode(value))
453 +
				.map((b) => b.toString(16).padStart(2, "0"))
454 +
				.join("");
455 +
			return hex.padEnd(64, "0");
456 +
		}
457 +
		return "".padStart(64, "0");
458 +
	}
459 +
460 +
	decodeResult(result) {
461 +
		if (!result || result === "0x") return null;
462 +
463 +
		// Simplified decoding - use proper ABI decoding library in production
464 +
		const outputs = this.methodData.outputs || [];
465 +
		if (outputs.length === 0) return result;
466 +
467 +
		const output = outputs[0];
468 +
		const data = result.replace("0x", "");
469 +
470 +
		if (output.type === "uint256" || output.type === "uint") {
471 +
			return BigInt("0x" + data).toString();
472 +
		} else if (output.type === "address") {
473 +
			return "0x" + data.slice(-40);
474 +
		} else if (output.type === "string") {
475 +
			// Simplified string decoding
476 +
			try {
477 +
				const bytes =
478 +
					data.match(/.{2}/g)?.map((byte) => parseInt(byte, 16)) || [];
479 +
				return new TextDecoder().decode(new Uint8Array(bytes));
480 +
			} catch (e) {
481 +
				console.log(e);
482 +
				return data;
483 +
			}
484 +
		}
485 +
486 +
		return result;
487 +
	}
488 +
489 +
	keccak256(input) {
490 +
		const inputBytes = new TextEncoder().encode(input);
491 +
		const hashBytes = keccak_256(inputBytes);
492 +
		return Array.from(hashBytes)
493 +
			.map((b) => b.toString(16).padStart(2, "0"))
494 +
			.join("");
495 +
	}
496 +
497 +
	// UI helper methods
498 +
	getCSSVariable(name, defaultValue) {
499 +
		return this.getAttribute(name) || defaultValue;
500 +
	}
501 +
502 +
	getStatusColor() {
503 +
		if (this.error) return this.getCSSVariable("error-color", "#E78A53");
504 +
		if (this.result !== null)
505 +
			return this.getCSSVariable("success-color", "#5F8787");
506 +
		return this.getCSSVariable("primary", "#5F8787");
507 +
	}
508 +
509 +
	// Render methods
510 +
	render() {
511 +
		const background = this.getCSSVariable("background", "#232323");
512 +
		const foreground = this.getCSSVariable("foreground", "#ffffff");
513 +
		const primary = this.getCSSVariable("primary", "#5F8787");
514 +
		const borderRadius = this.getCSSVariable("border-radius", "4px");
515 +
516 +
		this.shadowRoot.innerHTML = `
517 +
			<style>
518 +
				:host {
519 +
					--color-background: ${background};
520 +
					--color-foreground: ${foreground};
521 +
					--color-primary: ${primary};
522 +
					--border-radius: ${borderRadius};
523 +
					display: inline-block;
524 +
					font-family: sans-serif;
525 +
				}
526 +
527 +
				.container {
528 +
					display: flex;
529 +
					flex-direction: column;
530 +
					gap: 12px;
531 +
					padding: 16px;
532 +
					background: var(--color-background);
533 +
					border: 1px solid rgba(255, 255, 255, 0.1);
534 +
					border-radius: var(--border-radius);
535 +
					color: var(--color-foreground);
536 +
					width: 300px;
537 +
					box-sizing: border-box;
538 +
				}
539 +
540 +
				@media (max-width: 768px) {
541 +
					.container {
542 +
						width: 280px;
543 +
						padding: 12px;
544 +
						gap: 10px;
545 +
					}
546 +
				}
547 +
548 +
				@media (max-width: 480px) {
549 +
					.container {
550 +
						width: 260px;
551 +
						padding: 10px;
552 +
						gap: 8px;
553 +
					}
554 +
				}
555 +
556 +
				@media (max-width: 320px) {
557 +
					.container {
558 +
						width: 240px;
559 +
						padding: 8px;
560 +
						gap: 6px;
561 +
					}
562 +
				}
563 +
564 +
565 +
				.contract-info {
566 +
					display: flex;
567 +
					flex-direction: column;
568 +
					gap: 8px;
569 +
					font-size: 14px;
570 +
					opacity: 0.8;
571 +
				}
572 +
573 +
				.info-row {
574 +
					display: flex;
575 +
					flex-direction: column;
576 +
				}
577 +
578 +
				.info-label {
579 +
					font-weight: 600;
580 +
					margin-bottom: 4px;
581 +
				}
582 +
583 +
				.info-value {
584 +
					font-family: monospace;
585 +
					font-size: 12px;
586 +
					overflow: hidden;
587 +
					text-overflow: ellipsis;
588 +
					white-space: nowrap;
589 +
					max-width: 100%;
590 +
				}
591 +
592 +
				.status {
593 +
					font-size: 14px;
594 +
					font-family: monospace;
595 +
					word-break: break-all;
596 +
					color: ${this.getStatusColor()};
597 +
					overflow-wrap: break-word;
598 +
					white-space: pre-wrap;
599 +
				}
600 +
601 +
				.loading {
602 +
					display: flex;
603 +
					align-items: center;
604 +
					gap: 8px;
605 +
					color: var(--color-primary);
606 +
				}
607 +
608 +
				.spinner {
609 +
					width: 16px;
610 +
					height: 16px;
611 +
					border: 2px solid rgba(255, 255, 255, 0.3);
612 +
					border-top: 2px solid var(--color-foreground);
613 +
					border-radius: 50%;
614 +
					animation: spin 1s linear infinite;
615 +
				}
616 +
617 +
				@keyframes spin {
618 +
					from { transform: rotate(0deg); }
619 +
					to { transform: rotate(360deg); }
620 +
				}
621 +
622 +
				.error {
623 +
					color: ${this.getCSSVariable("error-color", "#E78A53")};
624 +
				}
625 +
626 +
				.success {
627 +
					color: ${this.getCSSVariable("success-color", "#5F8787")};
628 +
				}
629 +
630 +
				.result-label {
631 +
					font-weight: 600;
632 +
					margin-bottom: 4px;
633 +
					font-size: 14px;
634 +
				}
635 +
636 +
				.result-value {
637 +
					font-family: monospace;
638 +
					font-size: 14px;
639 +
					color: var(--color-primary);
640 +
				}
641 +
			</style>
642 +
		`;
643 +
644 +
		const container = document.createElement("div");
645 +
		container.className = "container";
646 +
647 +
		// Loading state
648 +
		if (this.loading) {
649 +
			const loadingDiv = document.createElement("div");
650 +
			loadingDiv.className = "loading";
651 +
			loadingDiv.innerHTML = `
652 +
				<div class="spinner"></div>
653 +
				<span>Reading contract...</span>
654 +
			`;
655 +
			container.appendChild(loadingDiv);
656 +
		}
657 +
658 +
		// Result or error section
659 +
		if (this.error) {
660 +
			const errorDiv = document.createElement("div");
661 +
			errorDiv.className = "status error";
662 +
			errorDiv.textContent = `Error: ${this.error}`;
663 +
			container.appendChild(errorDiv);
664 +
		} else if (this.result !== null && !this.loading) {
665 +
			const resultDiv = document.createElement("div");
666 +
			resultDiv.className = "info-row";
667 +
			resultDiv.innerHTML = `
668 +
				<span class="result-label">${this.methodName}</span>
669 +
				<span class="result-value">${this.result}</span>
670 +
			`;
671 +
			container.appendChild(resultDiv);
672 +
		}
673 +
674 +
		this.shadowRoot.appendChild(container);
675 +
	}
676 +
}
677 +
678 +
customElements.define("contract-read", ContractRead);