chore: updated connect-wallet 2753dc50
Steve · 2025-09-18 23:43 3 file(s) · +300 −115
components/connect-wallet.js +251 −108
6 6
		this.address = "";
7 7
		this.ensData = null;
8 8
		this.loading = false;
9 +
		this.chainId = "0x1";
10 +
		this.currentChainId = null;
11 +
	}
12 +
13 +
	static get observedAttributes() {
14 +
		return ["chain-id"];
15 +
	}
16 +
17 +
	attributeChangedCallback(name, oldValue, newValue) {
18 +
		if (name === "chain-id" && oldValue !== newValue) {
19 +
			this.chainId = newValue;
20 +
			// If already connected, check if we need to switch chains
21 +
			if (this.connected) {
22 +
				this.checkAndSwitchChain();
23 +
			}
24 +
		}
9 25
	}
10 26
11 27
	connectedCallback() {
28 +
		// Get chain-id from attribute
29 +
		this.chainId = this.getAttribute("chain-id");
12 30
		this.render();
13 31
	}
14 32
33 +
	async connect() {
34 +
		if (window.ethereum) {
35 +
			try {
36 +
				this.loading = true;
37 +
				this.render();
38 +
39 +
				const accounts = await window.ethereum.request({
40 +
					method: "eth_requestAccounts",
41 +
				});
42 +
43 +
				this.address = accounts[0];
44 +
45 +
				// Get current chain
46 +
				this.currentChainId = await window.ethereum.request({
47 +
					method: "eth_chainId",
48 +
				});
49 +
50 +
				// Switch to desired chain if specified
51 +
				if (this.chainId && this.chainId !== this.currentChainId) {
52 +
					await this.switchChain(this.chainId);
53 +
				}
54 +
55 +
				this.connected = true;
56 +
57 +
				await this.fetchEnsData();
58 +
59 +
				this.loading = false;
60 +
				this.render();
61 +
62 +
				// Dispatch custom event
63 +
				this.dispatchEvent(
64 +
					new CustomEvent("wallet-connected", {
65 +
						detail: {
66 +
							address: this.address,
67 +
							ensData: this.ensData,
68 +
							chainId: this.currentChainId,
69 +
						},
70 +
					}),
71 +
				);
72 +
			} catch (error) {
73 +
				console.error("Connection failed", error);
74 +
				this.loading = false;
75 +
				this.render();
76 +
77 +
				// Dispatch error event
78 +
				this.dispatchEvent(
79 +
					new CustomEvent("wallet-error", {
80 +
						detail: { error: error.message },
81 +
					}),
82 +
				);
83 +
			}
84 +
		} else {
85 +
			alert("Please install a wallet extension like MetaMask");
86 +
		}
87 +
	}
88 +
89 +
	async switchChain(chainId) {
90 +
		try {
91 +
			await window.ethereum.request({
92 +
				method: "wallet_switchEthereumChain",
93 +
				params: [{ chainId }],
94 +
			});
95 +
			this.currentChainId = chainId;
96 +
		} catch (switchError) {
97 +
			// Chain not added to wallet
98 +
			if (switchError.code === 4902) {
99 +
				try {
100 +
					await this.addChain(chainId);
101 +
				} catch (addError) {
102 +
					throw new Error(`Failed to add chain: ${addError.message}`);
103 +
				}
104 +
			} else {
105 +
				throw new Error(`Failed to switch chain: ${switchError.message}`);
106 +
			}
107 +
		}
108 +
	}
109 +
110 +
	async addChain(chainId) {
111 +
		const chainConfig = this.getChainConfig(chainId);
112 +
		if (!chainConfig) {
113 +
			throw new Error(`Unknown chain ID: ${chainId}`);
114 +
		}
115 +
116 +
		await window.ethereum.request({
117 +
			method: "wallet_addEthereumChain",
118 +
			params: [chainConfig],
119 +
		});
120 +
		this.currentChainId = chainId;
121 +
	}
122 +
123 +
	async checkAndSwitchChain() {
124 +
		if (window.ethereum && this.chainId && this.connected) {
125 +
			const currentChain = await window.ethereum.request({
126 +
				method: "eth_chainId",
127 +
			});
128 +
129 +
			if (currentChain !== this.chainId) {
130 +
				try {
131 +
					await this.switchChain(this.chainId);
132 +
					this.render();
133 +
				} catch (error) {
134 +
					console.error("Failed to switch chain:", error);
135 +
				}
136 +
			}
137 +
		}
138 +
	}
139 +
140 +
	getChainConfig(chainId) {
141 +
		const chains = {
142 +
			"0x1": {
143 +
				chainId: "0x1",
144 +
				chainName: "Ethereum Mainnet",
145 +
				rpcUrls: ["https://eth.drpc.org"],
146 +
				nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
147 +
				blockExplorerUrls: ["https://etherscan.io/"],
148 +
			},
149 +
			"0x89": {
150 +
				chainId: "0x89",
151 +
				chainName: "Polygon",
152 +
				rpcUrls: ["https://polygon.drpc.org"],
153 +
				nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
154 +
				blockExplorerUrls: ["https://polygonscan.com/"],
155 +
			},
156 +
			"0xa": {
157 +
				chainId: "0xa",
158 +
				chainName: "Optimism",
159 +
				rpcUrls: ["https://optimism.drpc.org"],
160 +
				nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
161 +
				blockExplorerUrls: ["https://optimistic.etherscan.io/"],
162 +
			},
163 +
			"0xa4b1": {
164 +
				chainId: "0xa4b1",
165 +
				chainName: "Arbitrum One",
166 +
				rpcUrls: ["https://arbitrum.drpc.org"],
167 +
				nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
168 +
				blockExplorerUrls: ["https://arbiscan.io/"],
169 +
			},
170 +
			"0x2105": {
171 +
				chainId: "0x2105",
172 +
				chainName: "Base",
173 +
				rpcUrls: ["https://base.drpc.org"],
174 +
				nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
175 +
				blockExplorerUrls: ["https://basescan.org/"],
176 +
			},
177 +
			// Add more chains as needed
178 +
		};
179 +
180 +
		return chains[chainId];
181 +
	}
182 +
183 +
	getChainName(chainId) {
184 +
		const chainNames = {
185 +
			"0x1": "Ethereum",
186 +
			"0x89": "Polygon",
187 +
			"0xa": "Optimism",
188 +
			"0xa4b1": "Arbitrum",
189 +
			"0x2105": "Base",
190 +
			// Add more as needed
191 +
		};
192 +
		return chainNames[chainId] || `Chain ${chainId}`;
193 +
	}
194 +
	renderProfile() {
195 +
		const profileDiv = document.createElement("div");
196 +
		profileDiv.className = "profile";
197 +
198 +
		const avatar = this.ensData?.avatar_small;
199 +
		const displayName = this.getDisplayName();
200 +
		const hasEns = this.ensData?.ens_primary;
201 +
		const addressToShow = this.truncateAddress(this.address);
202 +
203 +
		// Create avatar element
204 +
		let avatarElement = "";
205 +
		if (avatar) {
206 +
			avatarElement = `<img src="${avatar}" alt="Avatar" class="avatar" onerror="this.style.display='none'">`;
207 +
		} else {
208 +
			avatarElement = `<div class="avatar-placeholder"></div>`;
209 +
		}
210 +
211 +
		profileDiv.innerHTML = `
212 +
			${avatarElement}
213 +
			<div class="profile-info">
214 +
				<h3>${displayName}</h3>
215 +
				${hasEns ? `<p>${addressToShow}</p>` : ""}
216 +
			</div>
217 +
		`;
218 +
219 +
		profileDiv.addEventListener("click", () => {
220 +
			this.disconnect();
221 +
		});
222 +
223 +
		this.shadowRoot.appendChild(profileDiv);
224 +
	}
225 +
226 +
	async fetchEnsData() {
227 +
		try {
228 +
			const response = await fetch(`https://api.ensdata.net/${this.address}`);
229 +
			if (response.ok) {
230 +
				this.ensData = await response.json();
231 +
				console.log("ENS data loaded:", this.ensData);
232 +
			} else {
233 +
				console.log("No ENS data found for this address");
234 +
				this.ensData = null;
235 +
			}
236 +
		} catch (error) {
237 +
			console.error("Failed to fetch ENS data", error);
238 +
			this.ensData = null;
239 +
		}
240 +
	}
241 +
242 +
	disconnect() {
243 +
		this.connected = false;
244 +
		this.address = "";
245 +
		this.ensData = null;
246 +
		this.currentChainId = null;
247 +
		this.render();
248 +
249 +
		// Dispatch custom event
250 +
		this.dispatchEvent(new CustomEvent("wallet-disconnected"));
251 +
	}
252 +
253 +
	// Add CSS for chain display
15 254
	render() {
16 255
		this.shadowRoot.innerHTML = `
17 256
			<style>
132 371
		}
133 372
	}
134 373
374 +
	getDisplayName() {
375 +
		if (this.ensData?.ens_primary) {
376 +
			return this.ensData.ens_primary;
377 +
		}
378 +
		return this.truncateAddress(this.address);
379 +
	}
380 +
381 +
	truncateAddress(addr) {
382 +
		if (!addr) return "";
383 +
		return addr.slice(0, 6) + "..." + addr.slice(-4);
384 +
	}
385 +
135 386
	renderLoading() {
136 387
		const loadingDiv = document.createElement("div");
137 388
		loadingDiv.className = "profile loading";
147 398
		button.textContent = "Connect Wallet";
148 399
		button.addEventListener("click", () => this.connect());
149 400
		this.shadowRoot.appendChild(button);
150 -
	}
151 -
152 -
	renderProfile() {
153 -
		const profileDiv = document.createElement("div");
154 -
		profileDiv.className = "profile";
155 -
156 -
		const avatar = this.ensData?.avatar_small;
157 -
		const displayName = this.getDisplayName();
158 -
		const hasEns = this.ensData?.ens_primary;
159 -
		const addressToShow = this.truncateAddress(this.address);
160 -
161 -
		// Create avatar element
162 -
		let avatarElement = "";
163 -
		if (avatar) {
164 -
			avatarElement = `<img src="${avatar}" alt="Avatar" class="avatar" onerror="this.style.display='none'">`;
165 -
		} else {
166 -
			avatarElement = `<div class="avatar-placeholder"></div>`;
167 -
		}
168 -
169 -
		profileDiv.innerHTML = `
170 -
			${avatarElement}
171 -
			<div class="profile-info">
172 -
				<h3>${displayName}</h3>
173 -
				${hasEns ? `<p>${addressToShow}</p>` : ""}
174 -
			</div>
175 -
		`;
176 -
177 -
		profileDiv.addEventListener("click", () => {
178 -
			this.disconnect();
179 -
		});
180 -
181 -
		this.shadowRoot.appendChild(profileDiv);
182 -
	}
183 -
184 -
	async connect() {
185 -
		if (window.ethereum) {
186 -
			try {
187 -
				this.loading = true;
188 -
				this.render();
189 -
190 -
				const accounts = await window.ethereum.request({
191 -
					method: "eth_requestAccounts",
192 -
				});
193 -
194 -
				this.address = accounts[0];
195 -
				this.connected = true;
196 -
197 -
				// Fetch ENS data
198 -
				await this.fetchEnsData();
199 -
200 -
				this.loading = false;
201 -
				this.render();
202 -
203 -
				// Dispatch custom event
204 -
				this.dispatchEvent(
205 -
					new CustomEvent("wallet-connected", {
206 -
						detail: {
207 -
							address: this.address,
208 -
							ensData: this.ensData,
209 -
						},
210 -
					}),
211 -
				);
212 -
			} catch (error) {
213 -
				console.error("Connection failed", error);
214 -
				this.loading = false;
215 -
				this.render();
216 -
			}
217 -
		} else {
218 -
			alert("Please install a wallet extension like MetaMask");
219 -
		}
220 -
	}
221 -
222 -
	async fetchEnsData() {
223 -
		try {
224 -
			const response = await fetch(`https://api.ensdata.net/${this.address}`);
225 -
			if (response.ok) {
226 -
				this.ensData = await response.json();
227 -
				console.log("ENS data loaded:", this.ensData);
228 -
			} else {
229 -
				console.log("No ENS data found for this address");
230 -
				this.ensData = null;
231 -
			}
232 -
		} catch (error) {
233 -
			console.error("Failed to fetch ENS data", error);
234 -
			this.ensData = null;
235 -
		}
236 -
	}
237 -
238 -
	disconnect() {
239 -
		this.connected = false;
240 -
		this.address = "";
241 -
		this.ensData = null;
242 -
		this.render();
243 -
244 -
		// Dispatch custom event
245 -
		this.dispatchEvent(new CustomEvent("wallet-disconnected"));
246 -
	}
247 -
248 -
	getDisplayName() {
249 -
		if (this.ensData?.ens_primary) {
250 -
			return this.ensData.ens_primary;
251 -
		}
252 -
		return this.truncateAddress(this.address);
253 -
	}
254 -
255 -
	truncateAddress(addr) {
256 -
		if (!addr) return "";
257 -
		return addr.slice(0, 6) + "..." + addr.slice(-4);
258 401
	}
259 402
}
260 403
index.html +48 −6
12 12
      font-family: ui-monospace, "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
13 13
      min-height: 100vh;
14 14
      width: 100%;
15 +
      display: flex;
16 +
      flex-direction: column;
17 +
      align-items: center;
18 +
      justify-content: center;
15 19
    }
16 20
17 21
    .container {
22 +
      border: 1px solid white;
23 +
      border-radius: 5px;
24 +
      width: 400px;
25 +
      height: 400px;
26 +
      display: flex;
27 +
      align-items: center;
28 +
      justify-content: center;
29 +
    }
30 +
31 +
    .component-showcase {
18 32
      display: flex;
19 33
      flex-direction: column;
20 34
      align-items: center;
21 -
      justify-content: center;
22 -
      height: 100vh;
23 -
      width: 100%;
35 +
      gap: 1rem;
36 +
    }
37 +
38 +
    .component-label {
39 +
      font-size: 1.2rem;
40 +
      color: #ccc;
41 +
    }
42 +
43 +
    .code-snippet {
44 +
      background: #1e1e1e;
45 +
      border: 1px solid #333;
46 +
      border-radius: 3px;
47 +
      padding: 0.5rem;
48 +
      font-size: 0.8rem;
49 +
      color: #888;
50 +
      margin-top: 0.5rem;
51 +
      text-align: center;
52 +
    }
53 +
54 +
    .header {
55 +
      text-align: center;
56 +
      margin-bottom: 2rem;
24 57
    }
25 58
26 59
  </style>
27 60
</head>
28 61
<body>
29 -
  <div class="container">
30 -
  <h1>Norn</h1>
31 -
  <connect-wallet></connect-wallet>
62 +
  <div class="header">
63 +
    <h1>Norns</h1>
64 +
    <p>Interopable web components for dApps</p>
65 +
  </div>
66 +
  <div class="component-showcase">
67 +
    <div class="container">
68 +
      <connect-wallet></connect-wallet>
69 +
    </div>
70 +
    <div class="component-label">connect-wallet</div>
71 +
    <div class="code-snippet">
72 +
      npx norns add connect-wallet
73 +
    </div>
32 74
  </div>
33 75
34 76
  <script src="components/connect-wallet.js"></script>
package.json +1 −1
1 1
{
2 -
	"name": "eos",
2 +
	"name": "norns",
3 3
	"module": "index.ts",
4 4
	"type": "module",
5 5
	"devDependencies": {