Monorepo for Aesthetic.Computer aesthetic.computer
at main 353 lines 10 kB view raw
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}