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
JavaScript
;
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">×</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
});