Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408/** * cli-wallet.mjs - Tezos wallet integration for CLI tools * * Allows CLI users to: * - Connect their Tezos wallet via QR code (Temple/Kukai) * - Sign and pay for transactions themselves * - Persist wallet connection for future use * - Sync wallet address to MongoDB profile * * Connection methods: * 1. QR Code scanning (Temple Beacon P2P or Kukai WalletConnect) * 2. Manual address entry (fallback) */
import { TezosToolkit } from "@taquito/taquito";import { promises as fs } from "fs";import { join, dirname } from "path";import { fileURLToPath } from "url";import readline from "readline";
const __dirname = dirname(fileURLToPath(import.meta.url));
// Storage for wallet sessionconst WALLET_FILE = join(process.env.HOME, ".ac-tezos-wallet");
// Network configconst NETWORKS = { ghostnet: { rpc: "https://ghostnet.ecadinfra.com", tzkt: "https://api.ghostnet.tzkt.io", name: "Ghostnet (Testnet)", }, mainnet: { rpc: "https://mainnet.ecadinfra.com", tzkt: "https://api.tzkt.io", name: "Mainnet", },};
// Terminal colorsconst RESET = '\x1b[0m';const BOLD = '\x1b[1m';const DIM = '\x1b[2m';const CYAN = '\x1b[36m';const GREEN = '\x1b[32m';const YELLOW = '\x1b[33m';const RED = '\x1b[31m';
let tezos = null;let connectedAddress = null;let currentNetwork = "ghostnet";
/** * Initialize the Tezos toolkit */export async function init(network = "ghostnet") { currentNetwork = network; const config = NETWORKS[network]; tezos = new TezosToolkit(config.rpc); // Try to load saved session const session = await loadSession(); if (session?.address) { connectedAddress = session.address; currentNetwork = session.network || network; } return { tezos, address: connectedAddress };}
/** * Connect wallet - prompt user to enter their address */export async function connect(network = "ghostnet") { currentNetwork = network; const config = NETWORKS[network]; console.log(`\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════╗${RESET}`); console.log(`${BOLD}${CYAN}║ 🔷 Connect Tezos Wallet ║${RESET}`); console.log(`${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════╝${RESET}\n`); console.log(`${DIM}Network: ${config.name}${RESET}\n`); console.log(`Enter your Tezos wallet address or .tez domain:`); console.log(`${DIM}Examples: tz1abc..., jeffrey.tez, or just "jeffrey"${RESET}\n`); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); let input = await new Promise((resolve) => { rl.question(`${CYAN}Address/Domain: ${RESET}`, (answer) => { rl.close(); resolve(answer.trim()); }); }); let address = input; let domain = null; // Check if it's a domain (doesn't start with tz) if (!input.match(/^tz[123]/)) { console.log(`${DIM}Resolving domain...${RESET}`); const resolved = await resolveDomain(input, network); if (resolved) { address = resolved; domain = input.endsWith('.tez') ? input : `${input}.tez`; console.log(`${GREEN}✓${RESET} ${domain} → ${address.slice(0, 8)}...${address.slice(-6)}\n`); } else { console.log(`${RED}❌ Could not resolve "${input}" to a Tezos address${RESET}`); console.log(`${DIM}Make sure the domain exists on ${config.name}${RESET}\n`); return null; } } // Validate address format if (!address.match(/^(tz1|tz2|tz3)[1-9A-HJ-NP-Za-km-z]{33}$/)) { console.log(`${RED}❌ Invalid Tezos address format${RESET}\n`); return null; } // Verify address exists on chain const balance = await fetchBalance(address, network); if (balance === null) { console.log(`${YELLOW}⚠️ Could not verify address on ${config.name}${RESET}`); console.log(`${DIM}The address may be new or network may be unavailable${RESET}\n`); } connectedAddress = address; // Lookup domain if we didn't already resolve one if (!domain) { domain = await fetchDomain(address, network); } console.log(`\n${GREEN}✅ Wallet connected!${RESET}`); if (domain) { console.log(`${CYAN}Domain:${RESET} ${domain}`); } console.log(`${CYAN}Address:${RESET} ${address}`); if (balance !== null) { console.log(`${CYAN}Balance:${RESET} ${balance.toFixed(2)} ꜩ`); } console.log(`${CYAN}Network:${RESET} ${config.name}\n`); // Save session (include domain if known) await saveSession(address, network, domain); return address;}
/** * Connect wallet via QR code (Temple/Kukai) * Uses beacon-node.mjs or walletconnect-node.mjs under the hood */export async function connectViaQR(walletType = "temple", network = "ghostnet") { currentNetwork = network; const config = NETWORKS[network]; console.log(`\n${BOLD}${CYAN}╔════════════════════════════════════════════════════════════════╗${RESET}`); console.log(`${BOLD}${CYAN}║ 🔷 Connect Tezos Wallet via QR Code ║${RESET}`); console.log(`${BOLD}${CYAN}╚════════════════════════════════════════════════════════════════╝${RESET}\n`); console.log(`${DIM}Wallet: ${walletType === 'temple' ? 'Temple (Beacon P2P)' : 'Kukai (WalletConnect)'}${RESET}`); console.log(`${DIM}Network: ${config.name}${RESET}\n`); let address = null; let domain = null; try { if (walletType === "temple") { // Use Beacon P2P const { pairWallet } = await import("./beacon-node.mjs"); const result = await pairWallet(); if (result?.permissionResponse) { address = result.permissionResponse.address || result.permissionResponse.account?.address || result.permissionResponse.accountInfo?.address; } } else if (walletType === "kukai") { // Use WalletConnect if (!process.env.WALLETCONNECT_PROJECT_ID) { console.log(`${RED}✗ Missing WALLETCONNECT_PROJECT_ID${RESET}`); console.log(`${DIM}Get one free at https://cloud.walletconnect.com${RESET}\n`); return null; } const { pairWalletWC } = await import("./walletconnect-node.mjs"); const result = await pairWalletWC(); if (result?.accounts?.length > 0) { address = result.accounts[0].address; } } if (!address) { console.log(`${RED}✗ No address received from wallet${RESET}\n`); return null; } connectedAddress = address; // Lookup domain domain = await fetchDomain(address, network); console.log(`\n${GREEN}✅ Wallet connected via QR!${RESET}`); if (domain) { console.log(`${CYAN}Domain:${RESET} ${domain}`); } console.log(`${CYAN}Address:${RESET} ${address}`); console.log(`${CYAN}Network:${RESET} ${config.name}\n`); // Save session await saveSession(address, network, domain); return address; } catch (err) { // Handle user rejection gracefully if (err.message?.includes('rejected') || err.message?.includes('User') || err.message?.includes('timeout')) { console.log(`\n${YELLOW}⚠️ Connection declined or timed out${RESET}\n`); return null; } console.log(`${RED}✗ QR connection failed: ${err.message}${RESET}\n`); return null; }}
/** * Disconnect wallet */export async function disconnect() { connectedAddress = null; await fs.unlink(WALLET_FILE).catch(() => {}); console.log(`${GREEN}✅ Wallet disconnected${RESET}\n`);}
/** * Get connected address (or null) */export function getAddress() { return connectedAddress;}
/** * Check if connected */export function isConnected() { return connectedAddress !== null;}
/** * Get Tezos toolkit for operations */export function getTezos() { return tezos;}
/** * NOTE: CLI cannot sign transactions directly (no Beacon in Node.js) * For now, we use server-side minting where admin pays gas * and tokens go to the user's connected address. * * Future: Could integrate with Temple CLI or remote signing service */
/** * Fetch .tez domain for an address (reverse lookup) * NOTE: Always uses mainnet API since .tez domains only exist on mainnet */export async function fetchDomain(address, _network = "ghostnet") { try { // Always use mainnet TzKT - .tez domains only exist on mainnet const res = await fetch(`https://api.tzkt.io/v1/domains?address=${address}&reverse=true&select=name`); if (res.ok) { const data = await res.json(); if (data && data.length > 0) { return data[0]; } } } catch (err) { // Ignore } return null;}
/** * Resolve .tez domain to address * NOTE: Always uses mainnet API since .tez domains only exist on mainnet */export async function resolveDomain(domain, _network = "ghostnet") { try { // Normalize domain - add .tez if not present const normalizedDomain = domain.endsWith('.tez') ? domain : `${domain}.tez`; // Always use mainnet TzKT - .tez domains only exist on mainnet const res = await fetch(`https://api.tzkt.io/v1/domains?name=${normalizedDomain}&select=address`); if (res.ok) { const data = await res.json(); if (data && data.length > 0 && data[0].address) { return data[0].address; // Return just the address string } } } catch (err) { // Ignore } return null;}
/** * Fetch balance for an address */export async function fetchBalance(address, network = "ghostnet") { try { const rpc = NETWORKS[network].rpc; const res = await fetch(`${rpc}/chains/main/blocks/head/context/contracts/${address}/balance`); if (res.ok) { const mutez = await res.json(); return parseInt(mutez) / 1_000_000; } } catch (err) { // Ignore } return null;}
/** * Save wallet session */async function saveSession(address, network, domain = null) { try { await fs.writeFile(WALLET_FILE, JSON.stringify({ address, network, domain, connectedAt: new Date().toISOString(), }), "utf8"); } catch (err) { // Ignore }}
/** * Load saved session (for display only - actual session is in Beacon) */export async function loadSession() { try { const data = await fs.readFile(WALLET_FILE, "utf8"); return JSON.parse(data); } catch (err) { return null; }}
/** * Update user's Tezos address in AC database * First tries API endpoint, falls back to direct DB if that fails */export async function updateDatabaseAddress(address, network, token, userId = null) { // Try API endpoint first (works when dev server is running) try { const endpoint = process.env.AC_ENDPOINT || "https://localhost:8888"; const response = await fetch(`${endpoint}/api/update-tezos-address`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ address, network }), }); if (response.ok) { console.log(`${GREEN}✅ Saved address to AC profile${RESET}`); return true; } } catch (err) { // API not available, try direct DB } // Fallback: Direct database update (works in devcontainer) if (userId) { try { const { connect } = await import("../system/backend/database.mjs"); const { db } = await connect(); await db.collection("users").updateOne( { sub: userId }, { $set: { "tezos.address": address, "tezos.network": network, "tezos.connectedAt": new Date(), }, } ); console.log(`${GREEN}✅ Saved address to AC profile (direct)${RESET}`); return true; } catch (err) { console.log(`${DIM}⚠️ Could not save to profile: ${err.message}${RESET}`); } } return false;}