Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354/** * walletconnect-node.mjs - WalletConnect 2.0 client for Tezos * * Works with Kukai mobile wallet (and other WC2-compatible wallets) * * WalletConnect 2.0 uses: * - relay.walletconnect.com as the relay server * - X25519 key exchange for encryption * - JSON-RPC over websocket * - Tezos namespace: "tezos:ghostnet" or "tezos:mainnet" * * Note: SDK v2.17.0 has a heartbeat bug that throws after ~30s in Node.js * We catch this gracefully - pairing works if wallet scans promptly. */
import { SignClient } from "@walletconnect/sign-client";import qrcode from "qrcode-terminal";
// Gracefully handle WalletConnect heartbeat crash (SDK v2.17.0 bug)process.on('uncaughtException', (err) => { if (err.message?.includes('terminate is not a function')) { // Known WalletConnect SDK bug - ignore and continue return; } console.error('Uncaught exception:', err); process.exit(1);});
// ANSI colorsconst GREEN = "\x1b[32m";const RED = "\x1b[31m";const YELLOW = "\x1b[33m";const CYAN = "\x1b[36m";const DIM = "\x1b[2m";const BOLD = "\x1b[1m";const RESET = "\x1b[0m";
// WalletConnect Project ID - get one FREE at https://cloud.walletconnect.comconst PROJECT_ID = process.env.WALLETCONNECT_PROJECT_ID;
if (!PROJECT_ID) { console.log(`${RED}✗ No WalletConnect Project ID found${RESET}`); console.log(`\n${CYAN}To use WalletConnect 2.0 (for Kukai mobile):${RESET}`); console.log(` 1. Go to ${BOLD}https://cloud.walletconnect.com${RESET}`); console.log(` 2. Create a free account and project`); console.log(` 3. Copy your Project ID`); console.log(` 4. Set it: ${DIM}export WALLETCONNECT_PROJECT_ID=your_id_here${RESET}`); console.log(`\n${YELLOW}Or use Beacon P2P for Temple wallet instead:${RESET}`); console.log(` ${DIM}node beacon-node.mjs --pair${RESET}\n`); process.exit(1);}
// Tezos network configuration - CAIP-2 format// Kukai uses simple names: tezos:mainnet or tezos:ghostnetconst TEZOS_NETWORK = process.env.TEZOS_NETWORK || "mainnet";const TEZOS_CHAIN_ID = `tezos:${TEZOS_NETWORK}`;
// Supported methods for Tezosconst TEZOS_METHODS = [ "tezos_getAccounts", "tezos_send", "tezos_sign"];
// Supported eventsconst TEZOS_EVENTS = [];
/** * WalletConnect 2.0 Client for Tezos */export class WalletConnectClient { constructor() { this.client = null; this.session = null; }
/** * Initialize the WalletConnect SignClient */ async init() { console.log(`${DIM}Initializing WalletConnect 2.0...${RESET}`); this.client = await SignClient.init({ projectId: PROJECT_ID, metadata: { name: "Aesthetic Computer", description: "Creative coding platform", url: "https://aesthetic.computer", icons: ["https://aesthetic.computer/icon.png"] } });
// Set up event listeners this.setupListeners(); console.log(`${GREEN}✓${RESET} WalletConnect initialized`); return this; }
/** * Set up event listeners for session events */ setupListeners() { this.client.on("session_event", ({ event }) => { console.log(`${CYAN}Session event:${RESET}`, event); });
this.client.on("session_update", ({ topic, params }) => { console.log(`${CYAN}Session updated:${RESET}`, topic); const { namespaces } = params; const session = this.client.session.get(topic); this.session = { ...session, namespaces }; });
this.client.on("session_delete", () => { console.log(`${YELLOW}Session deleted${RESET}`); this.session = null; });
this.client.on("session_expire", ({ topic }) => { console.log(`${YELLOW}Session expired:${RESET}`, topic); this.session = null; }); }
/** * Connect to a wallet * Returns URI for QR code display */ async connect() { console.log(`${DIM}Creating connection request...${RESET}`);
const { uri, approval } = await this.client.connect({ requiredNamespaces: { tezos: { methods: TEZOS_METHODS, chains: [TEZOS_CHAIN_ID], events: TEZOS_EVENTS } } });
if (!uri) { throw new Error("No URI returned from connect()"); }
console.log(`${GREEN}✓${RESET} Connection URI created`); console.log(`${DIM}URI: ${uri.slice(0, 50)}...${RESET}`);
return { uri, approval }; }
/** * Wait for session approval from wallet */ async waitForApproval(approval) { console.log(`${YELLOW}⏳ Waiting for wallet approval...${RESET}`); try { this.session = await approval(); console.log(`${GREEN}✓${RESET} Session established!`); return this.session; } catch (err) { console.log(`${RED}✗${RESET} Session rejected: ${err.message}`); throw err; } }
/** * Get accounts from the connected wallet */ getAccounts() { if (!this.session) { throw new Error("No active session"); }
const accounts = this.session.namespaces.tezos?.accounts || []; return accounts.map(acc => { // Format: "tezos:ghostnet:tz1..." const parts = acc.split(":"); return { chain: parts[0], network: parts[1], address: parts[2] }; }); }
/** * Request signing a payload */ async signPayload(payload, account) { if (!this.session) { throw new Error("No active session"); }
console.log(`${DIM}Requesting signature...${RESET}`);
const result = await this.client.request({ topic: this.session.topic, chainId: TEZOS_CHAIN_ID, request: { method: "tezos_sign", params: { account: account, payload: payload } } });
return result; }
/** * Request sending an operation */ async sendOperation(operations, account) { if (!this.session) { throw new Error("No active session"); }
console.log(`${DIM}Requesting operation...${RESET}`);
const result = await this.client.request({ topic: this.session.topic, chainId: TEZOS_CHAIN_ID, request: { method: "tezos_send", params: { account: account, operations: operations } } });
return result; }
/** * Disconnect the session */ async disconnect() { if (!this.session) { return; }
await this.client.disconnect({ topic: this.session.topic, reason: { code: 6000, message: "User disconnected" } });
this.session = null; console.log(`${GREEN}✓${RESET} Disconnected`); }}
/** * Display QR code in terminal */export function displayQR(uri) { console.log(`\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════╗${RESET}`); console.log(`${BOLD}${CYAN}║ 📱 Scan with Kukai mobile (WalletConnect) ║${RESET}`); console.log(`${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════╝${RESET}\n`); // Generate QR synchronously to stdout qrcode.generate(uri, { small: true }); console.log(`\n\n${DIM}Waiting for wallet connection...${RESET}\n\n`);}
/** * Full WalletConnect pairing flow */export async function pairWalletWC() { console.log(`\n${BOLD}${CYAN}═══════════════════════════════════════════════════════════════════${RESET}`); console.log(`${BOLD}${CYAN} WalletConnect 2.0 Wallet Pairing (Kukai) ${RESET}`); console.log(`${BOLD}${CYAN}═══════════════════════════════════════════════════════════════════${RESET}\n`);
// Initialize client const client = await new WalletConnectClient().init();
// Create connection const { uri, approval } = await client.connect();
// Display QR code displayQR(uri);
// Wait for approval const session = await client.waitForApproval(approval);
// Get accounts const accounts = client.getAccounts(); console.log(`\n${GREEN}✓ Wallet connected!${RESET}`); console.log(` ${DIM}Network:${RESET} ${TEZOS_NETWORK}`); if (accounts.length > 0) { console.log(` ${DIM}Accounts:${RESET}`); accounts.forEach(acc => { console.log(` - ${acc.address}`); }); }
return { client, session, accounts };}
// CLI entry pointconst args = process.argv.slice(2);
if (args.includes("--pair") || args.includes("-p")) { pairWalletWC() .then(({ accounts, client }) => { console.log(`\n${GREEN}✓ Pairing complete!${RESET}`); if (accounts.length > 0) { console.log(` Address: ${accounts[0].address}`); } // Give the SDK a moment to settle, then exit cleanly setTimeout(() => process.exit(0), 500); }) .catch(err => { // Handle user rejection gracefully if (err.message?.includes('rejected') || err.message?.includes('User')) { console.log(`\n${YELLOW}Connection declined by user${RESET}`); process.exit(0); } console.error(`${RED}Error: ${err.message}${RESET}`); process.exit(1); });} else if (args.includes("--help") || args.includes("-h")) { console.log(`${BOLD}WalletConnect 2.0 Client for Tezos${RESET}
${CYAN}Usage:${RESET} node walletconnect-node.mjs [options]
${CYAN}Options:${RESET} --pair, -p Start wallet pairing (displays QR code) --help, -h Show this help message
${CYAN}Environment Variables:${RESET} WALLETCONNECT_PROJECT_ID Your WalletConnect project ID TEZOS_NETWORK Network to use (ghostnet/mainnet)
${CYAN}Supported Wallets:${RESET} - Kukai mobile - Any WalletConnect 2.0 compatible Tezos wallet`);} else { console.log(`Use --pair to start wallet pairing, or --help for more options`);}