UNPKG

magic-wallet-sdk

Version:

One-click wallets for Stacks blockchain - instant onboarding with upgrade paths to permanent wallets

1,515 lines (1,458 loc) 47.3 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { DEFAULT_CONFIG: () => DEFAULT_CONFIG, MagicWallet: () => MagicWallet, NETWORKS: () => NETWORKS, WALLET_PROVIDERS: () => WALLET_PROVIDERS, generateTemporaryWallet: () => generateTemporaryWallet, isValidStacksAddress: () => isValidStacksAddress, microStxToStx: () => microStxToStx, restoreWalletFromMnemonic: () => restoreWalletFromMnemonic, restoreWalletFromPrivateKey: () => restoreWalletFromPrivateKey, showSeedModal: () => showSeedModal, stxToMicroStx: () => stxToMicroStx }); module.exports = __toCommonJS(index_exports); // src/MagicWallet.ts var import_transactions2 = require("@stacks/transactions"); // src/utils.ts var import_transactions = require("@stacks/transactions"); function generateSimpleMnemonic() { const words = [ "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual" ]; const mnemonic = []; for (let i = 0; i < 12; i++) { mnemonic.push(words[Math.floor(Math.random() * words.length)]); } return mnemonic.join(" "); } function generateTemporaryWallet() { const mnemonic = generateSimpleMnemonic(); const privateKey = (0, import_transactions.makeRandomPrivKey)(); const publicKey = (0, import_transactions.getPublicKey)(privateKey); const address = (0, import_transactions.publicKeyToAddress)(import_transactions.AddressVersion.TestnetSingleSig, publicKey); return { address, privateKey: privateKey.data.toString(), publicKey: publicKey.data.toString(), mnemonic, type: "temporary", createdAt: Date.now() }; } function restoreWalletFromMnemonic(mnemonic) { try { const words = mnemonic.trim().split(/\s+/); if (words.length !== 12) { throw new Error("Mnemonic must be 12 words"); } const hash = mnemonic.split(" ").reduce((acc, word, index) => { return acc + word.charCodeAt(0) * (index + 1); }, 0); const seed = new Uint8Array(32); for (let i = 0; i < 32; i++) { seed[i] = (hash + i) % 256; } const privateKey = (0, import_transactions.createStacksPrivateKey)(Buffer.from(seed).toString("hex")); const publicKey = (0, import_transactions.getPublicKey)(privateKey); const address = (0, import_transactions.publicKeyToAddress)(import_transactions.AddressVersion.TestnetSingleSig, publicKey); return { address, privateKey: privateKey.data.toString(), publicKey: publicKey.data.toString(), mnemonic, type: "temporary", createdAt: Date.now() }; } catch (error) { throw new Error(`Invalid mnemonic: ${error}`); } } function restoreWalletFromPrivateKey(privateKeyHex) { try { const privateKey = (0, import_transactions.createStacksPrivateKey)(privateKeyHex); const publicKey = (0, import_transactions.getPublicKey)(privateKey); const address = (0, import_transactions.publicKeyToAddress)(import_transactions.AddressVersion.TestnetSingleSig, publicKey); return { address, privateKey: privateKeyHex, publicKey: publicKey.data.toString(), mnemonic: "", // Not available when importing from private key type: "temporary", createdAt: Date.now() }; } catch (error) { throw new Error(`Invalid private key: ${error}`); } } async function encryptWalletData(wallet, password) { const walletData = { privateKey: wallet.privateKey, mnemonic: wallet.mnemonic, address: wallet.address, publicKey: wallet.publicKey, createdAt: wallet.createdAt }; const encoded = Buffer.from(JSON.stringify(walletData)).toString("base64"); return encoded; } async function decryptWalletData(encryptedData, password) { try { const decoded = Buffer.from(encryptedData, "base64").toString("utf-8"); const walletData = JSON.parse(decoded); return { ...walletData, type: "temporary" }; } catch (error) { throw new Error(`Failed to decrypt wallet: ${error}`); } } function isValidStacksAddress(address) { try { return /^S[PT][0-9A-HJ-NP-Z]{37,40}$/.test(address); } catch { return false; } } function stxToMicroStx(stx) { return Math.floor(stx * 1e6); } function microStxToStx(microStx) { return microStx / 1e6; } // src/config.ts var import_network = require("@stacks/network"); var NETWORKS = { mainnet: new import_network.StacksMainnet(), testnet: new import_network.StacksTestnet(), devnet: new import_network.StacksDevnet() }; var FAUCET_ENDPOINTS = { testnet: "https://stacks-node-api.testnet.stacks.co/extended/v1/faucets/stx", devnet: "http://localhost:3999/extended/v1/faucets/stx" }; var DEFAULT_CONFIG = { network: "testnet", autoFund: true, fundAmount: 1e6, // 1 STX in micro-STX persistSession: true }; var WALLET_PROVIDERS = [ { id: "hiro", name: "Hiro Wallet", installUrl: "https://wallet.hiro.so/", icon: "\u{1F98A}" }, { id: "xverse", name: "Xverse", installUrl: "https://www.xverse.app/", icon: "\u{1F31F}" }, { id: "leather", name: "Leather", installUrl: "https://leather.io/", icon: "\u{1F536}" } ]; var STORAGE_KEYS = { WALLET_DATA: "magic_wallet_data", SESSION: "magic_wallet_session", CONFIG: "magic_wallet_config" }; // src/export-utils.ts async function generateQRCode(text) { try { const QRCode = await import("qrcode"); return await QRCode.toDataURL(text, { width: 200, margin: 2, color: { dark: "#000000", light: "#FFFFFF" } }); } catch (error) { console.warn("QR code generation failed:", error); return ""; } } async function generateWalletPDF(wallet, network, options = {}) { try { const { jsPDF } = await import("jspdf"); const pdf = new jsPDF(); const pageWidth = pdf.internal.pageSize.getWidth(); const pageHeight = pdf.internal.pageSize.getHeight(); let yPosition = 20; const lineHeight = 8; const sectionSpacing = 15; const title = options.title || "\u{1FA84} Magic Wallet Backup"; pdf.setFontSize(20); pdf.setFont("helvetica", "bold"); pdf.text(title, pageWidth / 2, yPosition, { align: "center" }); yPosition += sectionSpacing; if (options.includeTimestamp !== false) { pdf.setFontSize(10); pdf.setFont("helvetica", "normal"); pdf.text(`Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()}`, pageWidth / 2, yPosition, { align: "center" }); yPosition += sectionSpacing; } pdf.setFillColor(255, 240, 240); pdf.rect(10, yPosition - 5, pageWidth - 20, 25, "F"); pdf.setTextColor(200, 0, 0); pdf.setFontSize(12); pdf.setFont("helvetica", "bold"); pdf.text("\u26A0\uFE0F SECURITY WARNING", pageWidth / 2, yPosition + 5, { align: "center" }); pdf.setFontSize(9); pdf.setFont("helvetica", "normal"); pdf.text("Keep this backup secure! Anyone with access can control your wallet.", pageWidth / 2, yPosition + 12, { align: "center" }); pdf.text("Never share your private key or seed phrase with anyone.", pageWidth / 2, yPosition + 18, { align: "center" }); yPosition += 35; pdf.setTextColor(0, 0, 0); pdf.setFontSize(14); pdf.setFont("helvetica", "bold"); pdf.text("Wallet Information", 15, yPosition); yPosition += lineHeight + 5; pdf.setFontSize(10); pdf.setFont("helvetica", "normal"); pdf.setFont("helvetica", "bold"); pdf.text("Address:", 15, yPosition); pdf.setFont("helvetica", "normal"); pdf.text(wallet.address, 15, yPosition + lineHeight); yPosition += lineHeight * 2 + 5; pdf.setFont("helvetica", "bold"); pdf.text("Network:", 15, yPosition); pdf.setFont("helvetica", "normal"); pdf.text(network.toUpperCase(), 15, yPosition + lineHeight); yPosition += lineHeight * 2 + 5; pdf.setFont("helvetica", "bold"); pdf.text("Created:", 15, yPosition); pdf.setFont("helvetica", "normal"); pdf.text(new Date(wallet.createdAt).toLocaleString(), 15, yPosition + lineHeight); yPosition += lineHeight * 2 + sectionSpacing; pdf.setFillColor(240, 248, 255); const seedBoxHeight = 40; pdf.rect(10, yPosition - 5, pageWidth - 20, seedBoxHeight, "F"); pdf.setFontSize(14); pdf.setFont("helvetica", "bold"); pdf.text("\u{1F511} Recovery Phrase (Seed)", 15, yPosition + 5); yPosition += lineHeight + 8; pdf.setFontSize(9); pdf.setFont("helvetica", "normal"); pdf.text("Write down these 12 words in order. You can restore your wallet with this phrase.", 15, yPosition); yPosition += lineHeight + 3; const words = wallet.mnemonic.split(" "); const wordsPerRow = 3; const wordBoxWidth = (pageWidth - 40) / wordsPerRow; pdf.setFontSize(11); pdf.setFont("helvetica", "bold"); for (let i = 0; i < words.length; i++) { const row = Math.floor(i / wordsPerRow); const col = i % wordsPerRow; const x = 15 + col * wordBoxWidth; const y = yPosition + row * lineHeight; pdf.text(`${i + 1}. ${words[i]}`, x, y); } yPosition += Math.ceil(words.length / wordsPerRow) * lineHeight + sectionSpacing; if (options.includeQR) { try { const addressQR = await generateQRCode(wallet.address); const mnemonicQR = await generateQRCode(wallet.mnemonic); if (addressQR) { if (yPosition + 80 > pageHeight - 20) { pdf.addPage(); yPosition = 20; } pdf.setFontSize(12); pdf.setFont("helvetica", "bold"); pdf.text("QR Codes for Easy Import", 15, yPosition); yPosition += lineHeight + 10; pdf.setFontSize(10); pdf.setFont("helvetica", "bold"); pdf.text("Wallet Address:", 15, yPosition); pdf.addImage(addressQR, "PNG", 15, yPosition + 5, 40, 40); if (mnemonicQR) { pdf.text("Recovery Phrase:", 70, yPosition); pdf.addImage(mnemonicQR, "PNG", 70, yPosition + 5, 40, 40); } yPosition += 50; } } catch (error) { console.warn("Failed to add QR codes to PDF:", error); } } if (options.includeInstructions !== false) { if (yPosition + 60 > pageHeight - 20) { pdf.addPage(); yPosition = 20; } pdf.setFontSize(12); pdf.setFont("helvetica", "bold"); pdf.text("How to Restore Your Wallet", 15, yPosition); yPosition += lineHeight + 5; const instructions = [ "1. Install a Stacks wallet (Hiro, Xverse, or Leather)", '2. Choose "Import Wallet" or "Restore from seed phrase"', "3. Enter your 12-word recovery phrase in the correct order", "4. Set a strong password for your wallet", "5. Your wallet and all assets will be restored" ]; pdf.setFontSize(10); pdf.setFont("helvetica", "normal"); instructions.forEach((instruction) => { pdf.text(instruction, 15, yPosition); yPosition += lineHeight + 2; }); } const footerY = pageHeight - 15; pdf.setFontSize(8); pdf.setFont("helvetica", "italic"); pdf.text("Generated by Magic Wallet SDK - Keep this document secure!", pageWidth / 2, footerY, { align: "center" }); return pdf.output("blob"); } catch (error) { throw new Error(`PDF generation failed: ${error}`); } } function downloadBlob(blob, filename) { if (typeof window === "undefined" || typeof document === "undefined") { throw new Error( "PDF download is only available in browser environments. In Node.js, use generateWalletPDF() to get the blob and save it manually." ); } try { if ("showSaveFilePicker" in window) { window.showSaveFilePicker({ suggestedName: filename, types: [{ description: "PDF files", accept: { "application/pdf": [".pdf"] } }] }).then((fileHandle) => { return fileHandle.createWritable(); }).then((writable) => { return writable.write(blob).then(() => writable.close()); }).catch((error) => { traditionalDownload(blob, filename); }); } else { traditionalDownload(blob, filename); } } catch (error) { throw new Error(`Download failed: ${error}`); } } function traditionalDownload(blob, filename) { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; link.style.display = "none"; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } function generatePrintableHTML(wallet, network) { const words = wallet.mnemonic.split(" "); const wordsHTML = words.map( (word, index) => `<div class="word-box"><span class="word-number">${index + 1}</span><span class="word">${word}</span></div>` ).join(""); return ` <!DOCTYPE html> <html> <head> <title>Magic Wallet Backup</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .header { text-align: center; margin-bottom: 30px; } .warning { background: #fff5f5; border: 2px solid #fed7d7; padding: 15px; margin: 20px 0; border-radius: 8px; } .warning h3 { color: #c53030; margin-top: 0; } .info-section { margin: 20px 0; } .info-section h3 { color: #2d3748; border-bottom: 2px solid #e2e8f0; padding-bottom: 5px; } .seed-phrase { background: #f7fafc; padding: 20px; border-radius: 8px; margin: 20px 0; } .words-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; } .word-box { background: white; padding: 10px; border: 1px solid #e2e8f0; border-radius: 4px; text-align: center; } .word-number { font-size: 12px; color: #718096; display: block; } .word { font-weight: bold; font-size: 16px; } .footer { margin-top: 40px; text-align: center; font-size: 12px; color: #718096; } @media print { body { margin: 0; } .no-print { display: none; } } </style> </head> <body> <div class="header"> <h1>\u{1FA84} Magic Wallet Backup</h1> <p>Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()}</p> <button class="no-print" onclick="window.print()">\u{1F5A8}\uFE0F Print This Page</button> </div> <div class="warning"> <h3>\u26A0\uFE0F Security Warning</h3> <p>Keep this backup secure! Anyone with access to your recovery phrase can control your wallet.</p> <p>Never share your private key or seed phrase with anyone.</p> </div> <div class="info-section"> <h3>Wallet Information</h3> <p><strong>Address:</strong> ${wallet.address}</p> <p><strong>Network:</strong> ${network.toUpperCase()}</p> <p><strong>Created:</strong> ${new Date(wallet.createdAt).toLocaleString()}</p> </div> <div class="seed-phrase"> <h3>\u{1F511} Recovery Phrase</h3> <p>Write down these 12 words in order. You can restore your wallet with this phrase.</p> <div class="words-grid"> ${wordsHTML} </div> </div> <div class="info-section"> <h3>How to Restore Your Wallet</h3> <ol> <li>Install a Stacks wallet (Hiro, Xverse, or Leather)</li> <li>Choose "Import Wallet" or "Restore from seed phrase"</li> <li>Enter your 12-word recovery phrase in the correct order</li> <li>Set a strong password for your wallet</li> <li>Your wallet and all assets will be restored</li> </ol> </div> <div class="footer"> <p>Generated by Magic Wallet SDK - Keep this document secure!</p> </div> </body> </html> `; } // src/seed-modal.ts function showSeedModal(data, options = {}) { return new Promise((resolve) => { if (typeof document === "undefined") { console.log("Seed Modal requires browser environment"); console.log("Mnemonic:", data.mnemonic); resolve(); return; } const modal = createSeedModal(data, options, resolve); document.body.appendChild(modal); modal.focus(); const handleKeyDown = (e) => { if (e.key === "Escape") { closeSeedModal(modal, resolve); document.removeEventListener("keydown", handleKeyDown); } }; document.addEventListener("keydown", handleKeyDown); }); } function createSeedModal(data, options, resolve) { const { title = "\u{1F511} Your Wallet Seed Phrase", warning = "Keep this secure! Anyone with access can control your wallet.", showCopy = true, showDownloadTxt = true, showDownloadPdf = false } = options; const modalHTML = ` <div class="seed-modal-overlay" tabindex="0"> <div class="seed-modal-content"> <div class="seed-modal-header"> <h3>${title}</h3> <button class="seed-modal-close" aria-label="Close">&times;</button> </div> <div class="seed-modal-body"> <div class="seed-warning"> <span class="warning-icon">\u26A0\uFE0F</span> <p>${warning}</p> </div> <div class="wallet-info"> <p><strong>Address:</strong> <code class="wallet-address">${data.address}</code></p> <p><strong>Network:</strong> ${data.network.toUpperCase()}</p> ${data.createdAt ? `<p><strong>Created:</strong> ${new Date(data.createdAt).toLocaleString()}</p>` : ""} </div> <div class="seed-phrase-container"> <h4>Seed Phrase (12 Words)</h4> <div class="seed-words"> ${data.mnemonic.split(" ").map( (word, index) => `<div class="seed-word"> <span class="word-number">${index + 1}</span> <span class="word-text">${word}</span> </div>` ).join("")} </div> </div> <div class="seed-actions"> ${showCopy ? '<button class="btn btn-primary copy-btn">\u{1F4CB} Copy to Clipboard</button>' : ""} ${showDownloadTxt ? '<button class="btn btn-secondary download-txt-btn">\u{1F4C4} Download as TXT</button>' : ""} ${showDownloadPdf ? '<button class="btn btn-secondary download-pdf-btn">\u{1F4C4} Download as PDF</button>' : ""} </div> <div class="security-tips"> <h4>\u{1F6E1}\uFE0F Security Tips</h4> <ul> <li>Never share your seed phrase with anyone</li> <li>Store it offline in a secure location</li> <li>Consider writing it down on paper as backup</li> <li>Never enter it on suspicious websites</li> </ul> </div> </div> <div class="seed-modal-footer"> <button class="btn btn-gray close-btn">I've Saved It Securely</button> </div> </div> </div> `; const modalElement = document.createElement("div"); modalElement.innerHTML = modalHTML; const modal = modalElement.firstElementChild; addSeedModalStyles(); setupSeedModalEvents(modal, data, resolve); return modal; } function addSeedModalStyles() { if (document.getElementById("seed-modal-styles")) return; const styles = document.createElement("style"); styles.id = "seed-modal-styles"; styles.textContent = ` .seed-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 10000; backdrop-filter: blur(4px); } .seed-modal-content { background: white; border-radius: 12px; max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .seed-modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid #e2e8f0; } .seed-modal-header h3 { margin: 0; color: #2d3748; font-size: 20px; } .seed-modal-close { background: none; border: none; font-size: 24px; cursor: pointer; padding: 4px; color: #718096; line-height: 1; } .seed-modal-close:hover { color: #2d3748; } .seed-modal-body { padding: 24px; } .seed-warning { background: #fef5e7; border: 1px solid #f6ad55; border-radius: 8px; padding: 16px; margin-bottom: 20px; display: flex; align-items: flex-start; gap: 12px; } .warning-icon { font-size: 20px; flex-shrink: 0; } .seed-warning p { margin: 0; color: #c05621; font-weight: 500; } .wallet-info { background: #f7fafc; border-radius: 8px; padding: 16px; margin-bottom: 20px; } .wallet-info p { margin: 8px 0; color: #4a5568; } .wallet-address { background: #edf2f7; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 14px; word-break: break-all; } .seed-phrase-container { margin-bottom: 24px; } .seed-phrase-container h4 { margin: 0 0 16px 0; color: #2d3748; } .seed-words { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; background: #f7fafc; padding: 16px; border-radius: 8px; border: 2px solid #e2e8f0; } .seed-word { background: white; border: 1px solid #cbd5e0; border-radius: 6px; padding: 8px; display: flex; align-items: center; gap: 8px; font-family: monospace; } .word-number { background: #4299e1; color: white; font-size: 11px; padding: 2px 6px; border-radius: 4px; min-width: 20px; text-align: center; font-weight: bold; } .word-text { font-weight: 500; color: #2d3748; } .seed-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 24px; } .btn { padding: 10px 16px; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; } .btn-primary { background: #4299e1; color: white; } .btn-primary:hover { background: #3182ce; } .btn-secondary { background: #48bb78; color: white; } .btn-secondary:hover { background: #38a169; } .btn-gray { background: #718096; color: white; } .btn-gray:hover { background: #4a5568; } .security-tips { background: #ebf8ff; border: 1px solid #90cdf4; border-radius: 8px; padding: 16px; } .security-tips h4 { margin: 0 0 12px 0; color: #2c5282; } .security-tips ul { margin: 0; padding-left: 20px; color: #2c5282; } .security-tips li { margin-bottom: 4px; } .seed-modal-footer { padding: 16px 24px; border-top: 1px solid #e2e8f0; text-align: center; } .copy-success { background: #68d391 !important; color: white !important; } `; document.head.appendChild(styles); } function setupSeedModalEvents(modal, data, resolve) { const closeBtn = modal.querySelector(".seed-modal-close"); const footerCloseBtn = modal.querySelector(".close-btn"); const closeHandler = () => closeSeedModal(modal, resolve); closeBtn?.addEventListener("click", closeHandler); footerCloseBtn?.addEventListener("click", closeHandler); modal.addEventListener("click", (e) => { if (e.target === modal) closeHandler(); }); const copyBtn = modal.querySelector(".copy-btn"); if (copyBtn) { copyBtn.addEventListener("click", () => { copyToClipboard(data.mnemonic, copyBtn); }); } const downloadTxtBtn = modal.querySelector(".download-txt-btn"); if (downloadTxtBtn) { downloadTxtBtn.addEventListener("click", () => { downloadAsText(data); }); } const downloadPdfBtn = modal.querySelector(".download-pdf-btn"); if (downloadPdfBtn) { downloadPdfBtn.addEventListener("click", async () => { try { console.log("PDF download would be implemented here"); alert("PDF download feature would be implemented here using the existing generateWalletPDF function"); } catch (error) { alert("PDF download failed. Please try TXT download instead."); } }); } } function closeSeedModal(modal, resolve) { modal.style.opacity = "0"; setTimeout(() => { if (modal.parentNode) { modal.parentNode.removeChild(modal); } resolve(); }, 200); } async function copyToClipboard(text, button) { try { await navigator.clipboard.writeText(text); const originalText = button.textContent; button.textContent = "\u2705 Copied!"; button.classList.add("copy-success"); setTimeout(() => { button.textContent = originalText; button.classList.remove("copy-success"); }, 2e3); } catch (error) { const textArea = document.createElement("textarea"); textArea.value = text; document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); button.textContent = "\u2705 Copied!"; setTimeout(() => { button.textContent = "\u{1F4CB} Copy to Clipboard"; }, 2e3); } } function downloadAsText(data) { const content = `MAGIC WALLET BACKUP =================== \u26A0\uFE0F KEEP THIS SECURE \u26A0\uFE0F Address: ${data.address} Network: ${data.network.toUpperCase()} Created: ${data.createdAt ? new Date(data.createdAt).toLocaleString() : "Unknown"} Seed Phrase (12 Words): ${data.mnemonic} SECURITY WARNINGS: - Never share your seed phrase with anyone - Store this backup in a secure location - Anyone with this information can control your wallet Instructions for importing: 1. Open Hiro Wallet, Xverse, or Leather wallet 2. Choose "Import Wallet" or "Restore Wallet" 3. Enter your 12-word seed phrase 4. Your wallet will be restored with all funds Magic Wallet SDK - https://github.com/your-repo Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()} `; const blob = new Blob([content], { type: "text/plain" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `magic-wallet-backup-${data.address.slice(0, 8)}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.txt`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } // src/MagicWallet.ts var BrowserStorageAdapter = class { async getItem(key) { if (typeof localStorage !== "undefined") { return localStorage.getItem(key); } return null; } async setItem(key, value) { if (typeof localStorage !== "undefined") { localStorage.setItem(key, value); } } async removeItem(key) { if (typeof localStorage !== "undefined") { localStorage.removeItem(key); } } }; var MagicWallet = class { config; currentWallet = null; connection = null; storage; eventListeners = /* @__PURE__ */ new Map(); constructor(config = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; this.storage = config.storage || new BrowserStorageAdapter(); if (this.config.persistSession) { this.restoreSession(); } } /** * 🪄 Create a new temporary wallet (MVP Feature) */ async createTemporaryWallet() { try { const wallet = generateTemporaryWallet(); this.currentWallet = wallet; if (this.config.autoFund && this.config.network !== "mainnet") { await this.requestFaucetFunds(wallet.address); } if (this.config.persistSession) { await this.saveWalletToStorage(wallet); } this.updateConnection({ address: wallet.address, type: "temporary", connected: true, network: this.config.network }); this.emitEvent("wallet_created", { wallet }); return wallet; } catch (error) { throw new Error(`Failed to create temporary wallet: ${error}`); } } /** * 🔄 Restore wallet from mnemonic */ async restoreFromMnemonic(mnemonic) { try { const wallet = restoreWalletFromMnemonic(mnemonic); this.currentWallet = wallet; if (this.config.persistSession) { await this.saveWalletToStorage(wallet); } this.updateConnection({ address: wallet.address, type: "temporary", connected: true, network: this.config.network }); this.emitEvent("wallet_connected", { wallet }); return wallet; } catch (error) { throw new Error(`Failed to restore from mnemonic: ${error}`); } } /** * 🔄 Restore wallet from private key */ async restoreFromPrivateKey(privateKey) { try { const wallet = restoreWalletFromPrivateKey(privateKey); this.currentWallet = wallet; if (this.config.persistSession) { await this.saveWalletToStorage(wallet); } this.updateConnection({ address: wallet.address, type: "temporary", connected: true, network: this.config.network }); this.emitEvent("wallet_connected", { wallet }); return wallet; } catch (error) { throw new Error(`Failed to restore from private key: ${error}`); } } /** * 💰 Request faucet funds for testnet/devnet */ async requestFaucetFunds(address) { const targetAddress = address || this.currentWallet?.address; if (!targetAddress) { throw new Error("No wallet address available"); } if (this.config.network === "mainnet") { throw new Error("Faucet not available on mainnet"); } try { const faucetUrl = FAUCET_ENDPOINTS[this.config.network]; const faucetUrlWithParams = `${faucetUrl}?address=${targetAddress}&stacking=false`; const response = await fetch(faucetUrlWithParams, { method: "POST" }); if (response.ok) { try { const result = await response.json(); if (result.success !== false && result.txId) { this.emitEvent("wallet_funded", { address: targetAddress, txid: result.txId }); return { success: true, txid: result.txId, message: "Faucet funds requested successfully" }; } else { return { success: false, message: result.error || "Faucet request failed" }; } } catch (parseError) { return { success: false, message: `Failed to parse faucet response: ${parseError}` }; } } else { const errorText = await response.text(); return { success: false, message: `Faucet request failed (${response.status}): ${errorText}` }; } } catch (error) { return { success: false, message: `Faucet request failed: ${error}` }; } } /** * 💸 Send STX tokens */ async sendSTX(recipient, amount, options = {}) { if (!this.currentWallet) { throw new Error("No wallet connected"); } if (!isValidStacksAddress(recipient)) { throw new Error("Invalid recipient address"); } try { const network = NETWORKS[this.config.network]; const senderKey = this.currentWallet.privateKey; const nonce = await (0, import_transactions2.getNonce)(this.currentWallet.address, network); const txOptions = { recipient, amount: stxToMicroStx(amount), senderKey, network, memo: options.memo || "", nonce, anchorMode: import_transactions2.AnchorMode.Any }; const transaction = await (0, import_transactions2.makeSTXTokenTransfer)(txOptions); const broadcastResponse = await (0, import_transactions2.broadcastTransaction)(transaction, network); if (broadcastResponse.error) { throw new Error(broadcastResponse.reason || "Transaction failed"); } this.emitEvent("transactionSent", { txid: broadcastResponse.txid, recipient, amount }); return broadcastResponse.txid; } catch (error) { throw new Error(`Failed to send STX: ${error}`); } } /** * 📤 Export wallet data for migration (Phase 2 Feature) */ exportWallet(format = "json") { if (!this.currentWallet) { throw new Error("No wallet to export"); } const exportData = { privateKey: this.currentWallet.privateKey, mnemonic: this.currentWallet.mnemonic, address: this.currentWallet.address, format }; this.emitEvent("wallet_exported", { format }); return exportData; } /** * 📄 One-click PDF export of wallet backup (Enhanced Feature) * Downloads a secure PDF backup with seed phrase, QR codes, and instructions */ async exportWalletToPDF(options = {}) { if (!this.currentWallet) { throw new Error("No wallet to export"); } try { const pdfBlob = await generateWalletPDF( this.currentWallet, this.config.network, { includeQR: true, includeInstructions: true, includeBalance: true, includeTimestamp: true, ...options } ); const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19); const filename = `magic-wallet-backup-${this.currentWallet.address.slice(0, 8)}-${timestamp}.pdf`; downloadBlob(pdfBlob, filename); this.emitEvent("wallet_exported", { format: "pdf", filename }); console.log("\u2705 Wallet backup PDF generated successfully:", filename); } catch (error) { console.error("\u274C Failed to generate PDF backup:", error); throw new Error(`Failed to export wallet as PDF: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * 📄 Generate wallet PDF blob (for Node.js environments) * Returns the PDF blob without downloading it */ async generateWalletPDFBlob(options = {}) { if (!this.currentWallet) { throw new Error("No wallet to export"); } try { const pdfBlob = await generateWalletPDF( this.currentWallet, this.config.network, { includeQR: true, includeInstructions: true, includeBalance: true, includeTimestamp: true, ...options } ); this.emitEvent("wallet_exported", { format: "pdf-blob" }); return pdfBlob; } catch (error) { console.error("\u274C Failed to generate PDF blob:", error); throw new Error(`Failed to generate PDF blob: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * 🖨️ Generate printable HTML backup (Alternative to PDF) * Returns HTML content that can be printed or saved */ generatePrintableBackup() { if (!this.currentWallet) { throw new Error("No wallet to export"); } try { const html = generatePrintableHTML(this.currentWallet, this.config.network); this.emitEvent("wallet_exported", { format: "html" }); return html; } catch (error) { console.error("\u274C Failed to generate HTML backup:", error); throw new Error(`Failed to generate printable backup: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * 📱 Generate QR code for easy wallet import * Returns QR code data URL for the mnemonic phrase */ async generateWalletQR() { if (!this.currentWallet) { throw new Error("No wallet to generate QR for"); } try { const qrDataUrl = await generateQRCode(this.currentWallet.mnemonic); this.emitEvent("wallet_exported", { format: "qr" }); return qrDataUrl; } catch (error) { console.error("\u274C Failed to generate QR code:", error); throw new Error(`Failed to generate QR code: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * 🔑 Show seed phrase in a secure modal UI * Simple, clean modal with copy and download options */ async showSeedPhrase(options = {}) { if (!this.currentWallet) { throw new Error("No wallet to show seed phrase for"); } try { await showSeedModal({ address: this.currentWallet.address, mnemonic: this.currentWallet.mnemonic, network: this.config.network, createdAt: this.currentWallet.createdAt }, { showCopy: true, showDownloadTxt: true, showDownloadPdf: false, // Keep it simple ...options }); this.emitEvent("seed_displayed", { method: "modal" }); } catch (error) { console.error("\u274C Failed to show seed phrase modal:", error); throw new Error(`Failed to show seed phrase: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * 🔗 Get available wallet providers for upgrade */ getAvailableProviders() { return WALLET_PROVIDERS.map((provider) => ({ ...provider, isInstalled: this.checkProviderInstallation(provider.id) })); } /** * ⬆️ Guide user through wallet upgrade process */ getUpgradeInstructions(providerId) { const provider = WALLET_PROVIDERS.find((p) => p.id === providerId); if (!provider) { throw new Error("Unknown wallet provider"); } const exportData = this.exportWallet("mnemonic"); const steps = [ `Install ${provider.name} from ${provider.installUrl}`, 'Open the wallet and select "Import Wallet"', 'Choose "Import from seed phrase"', "Enter your 12-word recovery phrase", "Set up a password for your new wallet", "Your temporary wallet has been upgraded!" ]; this.emitEvent("wallet_upgraded", { provider: providerId }); return { steps, exportData }; } /** * 📊 Get current wallet info */ getWalletInfo() { return this.connection; } /** * 🔌 Disconnect current wallet */ async disconnect() { this.currentWallet = null; this.connection = null; if (this.config.persistSession) { await this.storage.removeItem(STORAGE_KEYS.WALLET_DATA); await this.storage.removeItem(STORAGE_KEYS.SESSION); } this.emitEvent("wallet_disconnected", {}); } /** * 📻 Event system */ on(event, callback) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event).push(callback); } off(event, callback) { const listeners = this.eventListeners.get(event); if (listeners) { const index = listeners.indexOf(callback); if (index > -1) { listeners.splice(index, 1); } } } /** * 💰 Get wallet STX balance */ async getBalance(address) { const walletAddress = address || this.currentWallet?.address; if (!walletAddress) { throw new Error("No wallet address available"); } try { const network = NETWORKS[this.config.network]; const url = `${network.coreApiUrl}/extended/v1/address/${walletAddress}/stx`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch balance: ${response.status}`); } const data = await response.json(); const balanceInMicroStx = parseInt(data.balance, 10); return microStxToStx(balanceInMicroStx); } catch (error) { console.error("\u274C Failed to get balance:", error); throw new Error(`Failed to get balance: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * 🚀 ONE-CLICK MAGIC: Create, fund, and connect wallet instantly for dApps * This is the main feature for dApp integration - everything happens automatically */ async oneClickConnect(options = {}) { const { appName = "Stacks dApp", fundingAmount = 1e6, // 1 STX default onProgress, autoShowSeed = false } = options; try { onProgress?.("Creating wallet...", 20); const wallet = await this.createTemporaryWallet(); onProgress?.("Wallet created!", 40); let funded = false; if (this.config.network !== "mainnet") { onProgress?.("Requesting testnet funds...", 60); const faucetResult = await this.requestFaucetFunds(); funded = faucetResult.success; if (funded) { onProgress?.("Wallet funded!", 80); } else { onProgress?.("Funding failed, but wallet is ready", 80); } } onProgress?.("Connecting to dApp...", 90); this.emitEvent("dapp_connected", { appName, wallet: wallet.address, network: this.config.network, funded, timestamp: Date.now() }); onProgress?.("Ready to explore!", 100); if (autoShowSeed) { setTimeout(() => { this.showSeedPhrase({ title: `\u{1F389} Welcome to ${appName}! Your wallet is ready!`, showCopy: true, showDownloadTxt: true }); }, 1e3); } return { wallet, connection: this.connection, funded, ready: true }; } catch (error) { onProgress?.("Failed to setup wallet", 0); throw new Error(`One-click connect failed: ${error instanceof Error ? error.message : "Unknown error"}`); } } /** * 🎯 Quick connect for existing users (restores previous session) */ async quickConnect(appName = "Stacks dApp") { if (this.config.persistSession) { await this.restoreSession(); if (this.currentWallet && this.connection) { this.emitEvent("dapp_reconnected", { appName, wallet: this.currentWallet.address, network: this.config.network }); return { wallet: this.currentWallet, connection: this.connection, isExisting: true }; } } const result = await this.oneClickConnect({ appName }); return { wallet: result.wallet, connection: result.connection, isExisting: false }; } /** * 🌐 Get wallet provider for dApp integration (mimics standard wallet interface) */ getWalletProvider() { return { isConnected: !!this.currentWallet, account: this.currentWallet?.address || null, network: this.config.network, connect: async () => { if (!this.currentWallet) { const result = await this.oneClickConnect(); return result.wallet.address; } return this.currentWallet.address; }, disconnect: async () => { await this.disconnect(); }, sendTransaction: async (txOptions) => { if (!this.currentWallet) { throw new Error("No wallet connected"); } if (txOptions.type === "STX") { return await this.sendSTX(txOptions.recipient, txOptions.amount, txOptions); } throw new Error("Unsupported transaction type"); }, signMessage: async (message) => { if (!this.currentWallet) { throw new Error("No wallet connected"); } return `signed_${message}_with_${this.currentWallet.address}`; } }; } // Private methods async saveWalletToStorage(wallet) { try { const encrypted = await encryptWalletData(wallet, "magic-wallet-key"); await this.storage.setItem(STORAGE_KEYS.WALLET_DATA, encrypted); const session = { address: wallet.address, network: this.config.network, timestamp: Date.now() }; await this.storage.setItem(STORAGE_KEYS.SESSION, JSON.stringify(session)); } catch (error) { console.warn("Failed to save wallet to storage:", error); } } async restoreSession() { try { const sessionData = await this.storage.getItem(STORAGE_KEYS.SESSION); const walletData = await this.storage.getItem(STORAGE_KEYS.WALLET_DATA); if (sessionData && walletData) { const session = JSON.parse(sessionData); const wallet = await decryptWalletData(walletData, "magic-wallet-key"); this.currentWallet = wallet; this.updateConnection({ address: wallet.address, type: "temporary", connected: true, network: session.network }); } } catch (error) { console.warn("Failed to restore session:", error); } } updateConnection(connection) { this.connection = connection; } emitEvent(type, data) { const event = { type, data, timestamp: Date.now() }; const listeners = this.eventListeners.get(type); if (listeners) { listeners.forEach((callback) => callback(event)); } } checkProviderInstallation(providerId) { return false; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { DEFAULT_CONFIG, MagicWallet, NETWORKS, WALLET_PROVIDERS, generateTemporaryWallet, isValidStacksAddress, microStxToStx, restoreWalletFromMnemonic, restoreWalletFromPrivateKey, showSeedModal, stxToMicroStx });