Monorepo for Aesthetic.Computer
aesthetic.computer
1/**
2 * walletconnect-node.mjs - WalletConnect 2.0 client for Tezos
3 *
4 * Works with Kukai mobile wallet (and other WC2-compatible wallets)
5 *
6 * WalletConnect 2.0 uses:
7 * - relay.walletconnect.com as the relay server
8 * - X25519 key exchange for encryption
9 * - JSON-RPC over websocket
10 * - Tezos namespace: "tezos:ghostnet" or "tezos:mainnet"
11 *
12 * Note: SDK v2.17.0 has a heartbeat bug that throws after ~30s in Node.js
13 * We catch this gracefully - pairing works if wallet scans promptly.
14 */
15
16import { SignClient } from "@walletconnect/sign-client";
17import qrcode from "qrcode-terminal";
18
19// Gracefully handle WalletConnect heartbeat crash (SDK v2.17.0 bug)
20process.on('uncaughtException', (err) => {
21 if (err.message?.includes('terminate is not a function')) {
22 // Known WalletConnect SDK bug - ignore and continue
23 return;
24 }
25 console.error('Uncaught exception:', err);
26 process.exit(1);
27});
28
29// ANSI colors
30const GREEN = "\x1b[32m";
31const RED = "\x1b[31m";
32const YELLOW = "\x1b[33m";
33const CYAN = "\x1b[36m";
34const DIM = "\x1b[2m";
35const BOLD = "\x1b[1m";
36const RESET = "\x1b[0m";
37
38// WalletConnect Project ID - get one FREE at https://cloud.walletconnect.com
39const PROJECT_ID = process.env.WALLETCONNECT_PROJECT_ID;
40
41if (!PROJECT_ID) {
42 console.log(`${RED}✗ No WalletConnect Project ID found${RESET}`);
43 console.log(`\n${CYAN}To use WalletConnect 2.0 (for Kukai mobile):${RESET}`);
44 console.log(` 1. Go to ${BOLD}https://cloud.walletconnect.com${RESET}`);
45 console.log(` 2. Create a free account and project`);
46 console.log(` 3. Copy your Project ID`);
47 console.log(` 4. Set it: ${DIM}export WALLETCONNECT_PROJECT_ID=your_id_here${RESET}`);
48 console.log(`\n${YELLOW}Or use Beacon P2P for Temple wallet instead:${RESET}`);
49 console.log(` ${DIM}node beacon-node.mjs --pair${RESET}\n`);
50 process.exit(1);
51}
52
53// Tezos network configuration - CAIP-2 format
54// Kukai uses simple names: tezos:mainnet or tezos:ghostnet
55const TEZOS_NETWORK = process.env.TEZOS_NETWORK || "mainnet";
56const TEZOS_CHAIN_ID = `tezos:${TEZOS_NETWORK}`;
57
58// Supported methods for Tezos
59const TEZOS_METHODS = [
60 "tezos_getAccounts",
61 "tezos_send",
62 "tezos_sign"
63];
64
65// Supported events
66const TEZOS_EVENTS = [];
67
68/**
69 * WalletConnect 2.0 Client for Tezos
70 */
71export class WalletConnectClient {
72 constructor() {
73 this.client = null;
74 this.session = null;
75 }
76
77 /**
78 * Initialize the WalletConnect SignClient
79 */
80 async init() {
81 console.log(`${DIM}Initializing WalletConnect 2.0...${RESET}`);
82
83 this.client = await SignClient.init({
84 projectId: PROJECT_ID,
85 metadata: {
86 name: "Aesthetic Computer",
87 description: "Creative coding platform",
88 url: "https://aesthetic.computer",
89 icons: ["https://aesthetic.computer/icon.png"]
90 }
91 });
92
93 // Set up event listeners
94 this.setupListeners();
95
96 console.log(`${GREEN}✓${RESET} WalletConnect initialized`);
97 return this;
98 }
99
100 /**
101 * Set up event listeners for session events
102 */
103 setupListeners() {
104 this.client.on("session_event", ({ event }) => {
105 console.log(`${CYAN}Session event:${RESET}`, event);
106 });
107
108 this.client.on("session_update", ({ topic, params }) => {
109 console.log(`${CYAN}Session updated:${RESET}`, topic);
110 const { namespaces } = params;
111 const session = this.client.session.get(topic);
112 this.session = { ...session, namespaces };
113 });
114
115 this.client.on("session_delete", () => {
116 console.log(`${YELLOW}Session deleted${RESET}`);
117 this.session = null;
118 });
119
120 this.client.on("session_expire", ({ topic }) => {
121 console.log(`${YELLOW}Session expired:${RESET}`, topic);
122 this.session = null;
123 });
124 }
125
126 /**
127 * Connect to a wallet
128 * Returns URI for QR code display
129 */
130 async connect() {
131 console.log(`${DIM}Creating connection request...${RESET}`);
132
133 const { uri, approval } = await this.client.connect({
134 requiredNamespaces: {
135 tezos: {
136 methods: TEZOS_METHODS,
137 chains: [TEZOS_CHAIN_ID],
138 events: TEZOS_EVENTS
139 }
140 }
141 });
142
143 if (!uri) {
144 throw new Error("No URI returned from connect()");
145 }
146
147 console.log(`${GREEN}✓${RESET} Connection URI created`);
148 console.log(`${DIM}URI: ${uri.slice(0, 50)}...${RESET}`);
149
150 return { uri, approval };
151 }
152
153 /**
154 * Wait for session approval from wallet
155 */
156 async waitForApproval(approval) {
157 console.log(`${YELLOW}⏳ Waiting for wallet approval...${RESET}`);
158
159 try {
160 this.session = await approval();
161 console.log(`${GREEN}✓${RESET} Session established!`);
162 return this.session;
163 } catch (err) {
164 console.log(`${RED}✗${RESET} Session rejected: ${err.message}`);
165 throw err;
166 }
167 }
168
169 /**
170 * Get accounts from the connected wallet
171 */
172 getAccounts() {
173 if (!this.session) {
174 throw new Error("No active session");
175 }
176
177 const accounts = this.session.namespaces.tezos?.accounts || [];
178 return accounts.map(acc => {
179 // Format: "tezos:ghostnet:tz1..."
180 const parts = acc.split(":");
181 return {
182 chain: parts[0],
183 network: parts[1],
184 address: parts[2]
185 };
186 });
187 }
188
189 /**
190 * Request signing a payload
191 */
192 async signPayload(payload, account) {
193 if (!this.session) {
194 throw new Error("No active session");
195 }
196
197 console.log(`${DIM}Requesting signature...${RESET}`);
198
199 const result = await this.client.request({
200 topic: this.session.topic,
201 chainId: TEZOS_CHAIN_ID,
202 request: {
203 method: "tezos_sign",
204 params: {
205 account: account,
206 payload: payload
207 }
208 }
209 });
210
211 return result;
212 }
213
214 /**
215 * Request sending an operation
216 */
217 async sendOperation(operations, account) {
218 if (!this.session) {
219 throw new Error("No active session");
220 }
221
222 console.log(`${DIM}Requesting operation...${RESET}`);
223
224 const result = await this.client.request({
225 topic: this.session.topic,
226 chainId: TEZOS_CHAIN_ID,
227 request: {
228 method: "tezos_send",
229 params: {
230 account: account,
231 operations: operations
232 }
233 }
234 });
235
236 return result;
237 }
238
239 /**
240 * Disconnect the session
241 */
242 async disconnect() {
243 if (!this.session) {
244 return;
245 }
246
247 await this.client.disconnect({
248 topic: this.session.topic,
249 reason: {
250 code: 6000,
251 message: "User disconnected"
252 }
253 });
254
255 this.session = null;
256 console.log(`${GREEN}✓${RESET} Disconnected`);
257 }
258}
259
260/**
261 * Display QR code in terminal
262 */
263export function displayQR(uri) {
264 console.log(`\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════╗${RESET}`);
265 console.log(`${BOLD}${CYAN}║ 📱 Scan with Kukai mobile (WalletConnect) ║${RESET}`);
266 console.log(`${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════╝${RESET}\n`);
267
268 // Generate QR synchronously to stdout
269 qrcode.generate(uri, { small: true });
270
271 console.log(`\n\n${DIM}Waiting for wallet connection...${RESET}\n\n`);
272}
273
274/**
275 * Full WalletConnect pairing flow
276 */
277export async function pairWalletWC() {
278 console.log(`\n${BOLD}${CYAN}═══════════════════════════════════════════════════════════════════${RESET}`);
279 console.log(`${BOLD}${CYAN} WalletConnect 2.0 Wallet Pairing (Kukai) ${RESET}`);
280 console.log(`${BOLD}${CYAN}═══════════════════════════════════════════════════════════════════${RESET}\n`);
281
282 // Initialize client
283 const client = await new WalletConnectClient().init();
284
285 // Create connection
286 const { uri, approval } = await client.connect();
287
288 // Display QR code
289 displayQR(uri);
290
291 // Wait for approval
292 const session = await client.waitForApproval(approval);
293
294 // Get accounts
295 const accounts = client.getAccounts();
296
297 console.log(`\n${GREEN}✓ Wallet connected!${RESET}`);
298 console.log(` ${DIM}Network:${RESET} ${TEZOS_NETWORK}`);
299
300 if (accounts.length > 0) {
301 console.log(` ${DIM}Accounts:${RESET}`);
302 accounts.forEach(acc => {
303 console.log(` - ${acc.address}`);
304 });
305 }
306
307 return { client, session, accounts };
308}
309
310// CLI entry point
311const args = process.argv.slice(2);
312
313if (args.includes("--pair") || args.includes("-p")) {
314 pairWalletWC()
315 .then(({ accounts, client }) => {
316 console.log(`\n${GREEN}✓ Pairing complete!${RESET}`);
317 if (accounts.length > 0) {
318 console.log(` Address: ${accounts[0].address}`);
319 }
320 // Give the SDK a moment to settle, then exit cleanly
321 setTimeout(() => process.exit(0), 500);
322 })
323 .catch(err => {
324 // Handle user rejection gracefully
325 if (err.message?.includes('rejected') || err.message?.includes('User')) {
326 console.log(`\n${YELLOW}Connection declined by user${RESET}`);
327 process.exit(0);
328 }
329 console.error(`${RED}Error: ${err.message}${RESET}`);
330 process.exit(1);
331 });
332} else if (args.includes("--help") || args.includes("-h")) {
333 console.log(`
334${BOLD}WalletConnect 2.0 Client for Tezos${RESET}
335
336${CYAN}Usage:${RESET}
337 node walletconnect-node.mjs [options]
338
339${CYAN}Options:${RESET}
340 --pair, -p Start wallet pairing (displays QR code)
341 --help, -h Show this help message
342
343${CYAN}Environment Variables:${RESET}
344 WALLETCONNECT_PROJECT_ID Your WalletConnect project ID
345 TEZOS_NETWORK Network to use (ghostnet/mainnet)
346
347${CYAN}Supported Wallets:${RESET}
348 - Kukai mobile
349 - Any WalletConnect 2.0 compatible Tezos wallet
350`);
351} else {
352 console.log(`Use --pair to start wallet pairing, or --help for more options`);
353}