885 lines
34 KiB
JavaScript
885 lines
34 KiB
JavaScript
/* .SOL DOMAINS — Solana Name Service Lookup & Registration */
|
|
|
|
const SNS_API = 'https://sns-sdk-proxy.bonfida.workers.dev';
|
|
const SNS_REG = 'https://www.sns.id/domain/';
|
|
const SNS_REGISTER_API = 'https://sdk-proxy.sns.id';
|
|
const SOLSCAN = 'https://solscan.io/account/';
|
|
const SOLSCAN_TX = 'https://solscan.io/tx/';
|
|
const REFERRAL_WALLET = '9NuiHh5wgRPx69BFGP1ZR8kHiBENGoJrXs5GpZzKAyn8';
|
|
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
|
const SOLANA_RPC = 'https://api.mainnet-beta.solana.com';
|
|
|
|
let walletAddress = null;
|
|
let currentTab = 'search';
|
|
let pendingRegistrationDomain = null;
|
|
|
|
// ─── DOM ────────────────────────────────────────────────────
|
|
const $ = s => document.querySelector(s);
|
|
const $$ = s => document.querySelectorAll(s);
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initTabs();
|
|
initSearch();
|
|
initWallet();
|
|
createRegistrationModal();
|
|
});
|
|
|
|
// ─── TABS ───────────────────────────────────────────────────
|
|
function initTabs() {
|
|
$$('.sol-tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
const t = tab.dataset.tab;
|
|
switchTab(t);
|
|
});
|
|
});
|
|
}
|
|
|
|
function switchTab(t) {
|
|
currentTab = t;
|
|
$$('.sol-tab').forEach(tab => tab.classList.toggle('active', tab.dataset.tab === t));
|
|
$$('.sol-panel').forEach(p => p.classList.toggle('hidden', p.id !== `panel-${t}`));
|
|
if (t === 'mydomains' && walletAddress) loadMyDomains();
|
|
}
|
|
|
|
// ─── SEARCH / LOOKUP ────────────────────────────────────────
|
|
function initSearch() {
|
|
const input = $('#sol-search');
|
|
const btn = $('#sol-search-go');
|
|
if (!input || !btn) return;
|
|
btn.addEventListener('click', () => doSearch());
|
|
input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
|
|
|
|
const revInput = $('#sol-reverse');
|
|
const revBtn = $('#sol-reverse-go');
|
|
if (revInput && revBtn) {
|
|
revBtn.addEventListener('click', () => doReverse());
|
|
revInput.addEventListener('keydown', e => { if (e.key === 'Enter') doReverse(); });
|
|
}
|
|
}
|
|
|
|
async function doSearch() {
|
|
const raw = $('#sol-search').value.trim().toLowerCase();
|
|
if (!raw) return;
|
|
const domain = raw.replace(/\.sol$/i, '');
|
|
const results = $('#search-results');
|
|
results.innerHTML = loadingHTML('RESOLVING DOMAIN...');
|
|
|
|
try {
|
|
const res = await fetch(`${SNS_API}/resolve/${domain}`);
|
|
const data = await res.json();
|
|
if (data.s === 'ok' && data.result) {
|
|
await showTakenDomain(domain, data.result);
|
|
} else {
|
|
showAvailableDomain(domain);
|
|
}
|
|
} catch (err) {
|
|
results.innerHTML = errorHTML(`Network error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function showTakenDomain(domain, owner) {
|
|
const results = $('#search-results');
|
|
let favourite = null;
|
|
try {
|
|
const favRes = await fetch(`${SNS_API}/favourite-domain/${owner}`);
|
|
const favData = await favRes.json();
|
|
if (favData.s === 'ok') favourite = favData.result || null;
|
|
} catch(e) {}
|
|
|
|
results.innerHTML = `
|
|
<div class="sol-result-card">
|
|
<div class="sol-result-header">
|
|
<div class="sol-result-domain">${esc(domain)}<span class="sol-ext">.sol</span></div>
|
|
<div class="sol-result-status taken">REGISTERED</div>
|
|
</div>
|
|
<div class="sol-result-body">
|
|
<div>
|
|
<div class="sol-result-field">
|
|
<span class="sol-result-label">OWNER</span>
|
|
<div class="sol-result-value">
|
|
<a href="${SOLSCAN}${esc(owner)}" target="_blank" rel="noopener">${truncAddr(owner)}</a>
|
|
</div>
|
|
</div>
|
|
${favourite ? `
|
|
<div class="sol-result-field">
|
|
<span class="sol-result-label">OWNER'S FAVOURITE</span>
|
|
<div class="sol-result-value">${esc(favourite)}.sol</div>
|
|
</div>` : ''}
|
|
</div>
|
|
<div>
|
|
<div class="sol-result-field">
|
|
<span class="sol-result-label">SOLSCAN</span>
|
|
<div class="sol-result-value">
|
|
<a href="${SOLSCAN}${esc(owner)}" target="_blank" rel="noopener">View on Solscan ↗</a>
|
|
</div>
|
|
</div>
|
|
<div class="sol-result-field">
|
|
<span class="sol-result-label">SNS</span>
|
|
<div class="sol-result-value">
|
|
<a href="${SNS_REG}${esc(domain)}" target="_blank" rel="noopener">View on SNS.id ↗</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function showAvailableDomain(domain) {
|
|
const results = $('#search-results');
|
|
const price = getEstimatedPrice(domain);
|
|
|
|
results.innerHTML = `
|
|
<div class="sol-result-card">
|
|
<div class="sol-result-header">
|
|
<div class="sol-result-domain">${esc(domain)}<span class="sol-ext">.sol</span></div>
|
|
<div class="sol-result-status available">AVAILABLE</div>
|
|
</div>
|
|
<div class="sol-result-body">
|
|
<div>
|
|
<div class="sol-result-field">
|
|
<span class="sol-result-label">LENGTH</span>
|
|
<div class="sol-result-value">${domain.length} character${domain.length !== 1 ? 's' : ''}</div>
|
|
</div>
|
|
<div class="sol-result-field">
|
|
<span class="sol-result-label">ESTIMATED COST</span>
|
|
<div class="sol-price">
|
|
<span class="sol-price-amount">${price.amount.replace('~','')}</span>
|
|
<span class="sol-price-currency">${price.currency}</span>
|
|
<span class="sol-price-usd">(estimated)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="sol-result-field">
|
|
<span class="sol-result-label">REGISTRATION</span>
|
|
<div class="sol-result-value">Register directly from your wallet</div>
|
|
</div>
|
|
<div class="sol-result-field">
|
|
<span class="sol-result-label">PAYMENT</span>
|
|
<div class="sol-result-value">USDC on Solana</div>
|
|
</div>
|
|
<button class="sol-register-btn" id="register-domain-btn">REGISTER ${esc(domain.toUpperCase())}.SOL</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const regBtn = results.querySelector('#register-domain-btn');
|
|
if (regBtn) {
|
|
regBtn.addEventListener('click', () => initiateRegistration(domain));
|
|
}
|
|
}
|
|
|
|
// ─── REVERSE LOOKUP ─────────────────────────────────────────
|
|
async function doReverse() {
|
|
const addr = $('#sol-reverse').value.trim();
|
|
if (!addr) return;
|
|
|
|
const results = $('#reverse-results');
|
|
results.innerHTML = loadingHTML('SCANNING WALLET...');
|
|
|
|
try {
|
|
const res = await fetch(`${SNS_API}/domains/${addr}`);
|
|
const data = await res.json();
|
|
|
|
// Normalise response to array — API may return various formats
|
|
let domains = [];
|
|
if (Array.isArray(data)) {
|
|
domains = data;
|
|
} else if (data && typeof data === 'object') {
|
|
if (Array.isArray(data.result)) {
|
|
domains = data.result;
|
|
} else if (typeof data.result === 'string') {
|
|
domains = [data.result];
|
|
} else if (data.result && typeof data.result === 'object' && !Array.isArray(data.result)) {
|
|
// Single domain object
|
|
domains = [data.result];
|
|
}
|
|
}
|
|
|
|
if (!domains || domains.length === 0) {
|
|
results.innerHTML = `<div class="sol-empty">NO .SOL DOMAINS FOUND FOR THIS WALLET</div>`;
|
|
return;
|
|
}
|
|
|
|
let favourite = null;
|
|
try {
|
|
const favRes = await fetch(`${SNS_API}/favourite-domain/${addr}`);
|
|
const favData = await favRes.json();
|
|
if (favData.s === 'ok') favourite = favData.result || null;
|
|
} catch(e) {}
|
|
|
|
results.innerHTML = `
|
|
<div class="sol-stats-bar">
|
|
<div class="sol-stat">DOMAINS: <span class="sol-stat-value">${domains.length}</span></div>
|
|
${favourite ? `<div class="sol-stat">FAVOURITE: <span class="sol-stat-value">${favourite}.sol</span></div>` : ''}
|
|
<div class="sol-stat">WALLET: <span class="sol-stat-value">${truncAddr(addr)}</span></div>
|
|
</div>
|
|
<div class="sol-domains-grid">
|
|
${domains.map(d => {
|
|
const name = typeof d === 'string' ? d : (d.domain || d.name || String(d));
|
|
const isFav = favourite && name === favourite;
|
|
return `
|
|
<div class="sol-domain-card">
|
|
<div class="sol-domain-name">${esc(name)}<span class="sol-ext">.sol</span></div>
|
|
${isFav ? '<div class="sol-domain-fav">★ FAVOURITE</div>' : ''}
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
`;
|
|
} catch(err) {
|
|
results.innerHTML = errorHTML(`Network error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// ─── WALLET (uses global window.solWallet from wallet-connect.js) ────────────
|
|
|
|
// Sync local walletAddress from global wallet state
|
|
function syncWalletState() {
|
|
const sw = window.solWallet;
|
|
if (sw && sw.connected && sw.address) {
|
|
walletAddress = sw.address;
|
|
} else {
|
|
walletAddress = null;
|
|
}
|
|
}
|
|
|
|
function initWallet() {
|
|
const btn = $('#wallet-btn');
|
|
if (!btn) return;
|
|
btn.addEventListener('click', toggleWallet);
|
|
createWalletModal();
|
|
|
|
// Listen for global wallet events
|
|
window.addEventListener('wallet-connected', (e) => {
|
|
walletAddress = e.detail.address;
|
|
updateWalletUI();
|
|
checkPendingRegistration();
|
|
});
|
|
|
|
window.addEventListener('wallet-disconnected', () => {
|
|
walletAddress = null;
|
|
updateWalletUI();
|
|
});
|
|
|
|
// Sync initial state (wallet may have auto-reconnected already)
|
|
syncWalletState();
|
|
updateWalletUI();
|
|
}
|
|
|
|
function createWalletModal() {
|
|
const modal = document.createElement('div');
|
|
modal.id = 'wallet-modal';
|
|
modal.className = 'sol-modal hidden';
|
|
modal.innerHTML = `
|
|
<div class="sol-modal-backdrop"></div>
|
|
<div class="sol-modal-content">
|
|
<div class="sol-modal-header">
|
|
<span class="sol-modal-title">SELECT WALLET</span>
|
|
<button class="sol-modal-close" id="modal-close">✕</button>
|
|
</div>
|
|
<div class="sol-modal-body" id="wallet-list"></div>
|
|
<div class="sol-modal-footer">
|
|
<span class="sol-modal-hint">No wallet? Install one to connect to Solana</span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
modal.querySelector('.sol-modal-backdrop').addEventListener('click', closeModal);
|
|
modal.querySelector('#modal-close').addEventListener('click', closeModal);
|
|
}
|
|
|
|
function openModal() {
|
|
const sw = window.solWallet;
|
|
if (!sw) { console.warn('solWallet not loaded'); return; }
|
|
|
|
const modal = $('#wallet-modal');
|
|
const list = $('#wallet-list');
|
|
|
|
const KNOWN_WALLETS = sw.KNOWN_WALLETS || [];
|
|
|
|
// Small delay to let async provider injection complete
|
|
setTimeout(() => {
|
|
const available = sw.getAvailableWallets();
|
|
const detectedNames = new Set(available.map(w => w.name));
|
|
|
|
let html = '';
|
|
|
|
if (available.length > 0) {
|
|
html += '<div class="sol-wallet-divider">DETECTED</div>';
|
|
html += available.map(w => `
|
|
<button class="sol-wallet-option detected" data-wallet="${w.name}">
|
|
<span class="sol-wallet-option-icon">${w.icon}</span>
|
|
<span class="sol-wallet-option-name">${w.name}</span>
|
|
<span class="sol-wallet-option-status detected">DETECTED</span>
|
|
</button>
|
|
`).join('');
|
|
}
|
|
|
|
const notInstalled = KNOWN_WALLETS.filter(w => !detectedNames.has(w.name));
|
|
if (notInstalled.length > 0) {
|
|
html += '<div class="sol-wallet-divider">NOT INSTALLED</div>';
|
|
html += notInstalled.map(w => `
|
|
<a href="${w.url}" target="_blank" rel="noopener" class="sol-wallet-option not-installed">
|
|
<span class="sol-wallet-option-icon">${w.icon}</span>
|
|
<span class="sol-wallet-option-name">${w.name}</span>
|
|
<span class="sol-wallet-option-status">INSTALL ↗</span>
|
|
</a>
|
|
`).join('');
|
|
}
|
|
|
|
if (available.length === 0) {
|
|
html = `<div class="sol-empty" style="padding:1rem;">
|
|
NO SOLANA WALLETS DETECTED<br><br>
|
|
Install a Solana wallet extension to connect.<br>
|
|
<span style="color:#14F195;font-size:0.6rem;">If you just installed one, refresh the page.</span>
|
|
</div>`;
|
|
}
|
|
|
|
list.innerHTML = html;
|
|
|
|
list.querySelectorAll('.sol-wallet-option.detected').forEach(btn => {
|
|
btn.addEventListener('click', async (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const name = btn.dataset.wallet;
|
|
const wallet = available.find(w => w.name === name);
|
|
if (wallet) {
|
|
try {
|
|
closeModal();
|
|
await sw.connect(wallet);
|
|
} catch (err) {
|
|
console.error('Wallet connection failed:', err);
|
|
const status = $('#wallet-status');
|
|
if (status) status.innerHTML = `<span style="color:rgba(255,50,50,0.8)">CONNECTION REJECTED</span>`;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
modal.classList.remove('hidden');
|
|
}, 150);
|
|
}
|
|
|
|
function closeModal() {
|
|
const modal = $('#wallet-modal');
|
|
if (modal) modal.classList.add('hidden');
|
|
}
|
|
|
|
async function toggleWallet() {
|
|
if (walletAddress) {
|
|
disconnectWallet();
|
|
return;
|
|
}
|
|
openModal();
|
|
}
|
|
|
|
function disconnectWallet() {
|
|
const sw = window.solWallet;
|
|
if (sw) sw.disconnect();
|
|
walletAddress = null;
|
|
updateWalletUI();
|
|
}
|
|
|
|
function updateWalletUI() {
|
|
const status = $('#wallet-status');
|
|
const btn = $('#wallet-btn');
|
|
|
|
const sw = window.solWallet;
|
|
|
|
if (walletAddress) {
|
|
const wName = (sw && sw.walletName) ? sw.walletName : 'WALLET';
|
|
const wIcon = '◆';
|
|
status.className = 'sol-wallet-status connected';
|
|
status.innerHTML = `● CONNECTED VIA ${wIcon} ${wName} <span class="sol-wallet-address">${truncAddr(walletAddress)}</span>`;
|
|
btn.className = 'sol-wallet-btn disconnect';
|
|
btn.textContent = 'DISCONNECT';
|
|
} else {
|
|
status.className = 'sol-wallet-status';
|
|
status.innerHTML = '○ NOT CONNECTED';
|
|
btn.className = 'sol-wallet-btn';
|
|
btn.textContent = 'CONNECT WALLET';
|
|
const panel = $('#mydomains-content');
|
|
if (panel) panel.innerHTML = `<div class="sol-empty">CONNECT WALLET TO VIEW YOUR DOMAINS</div>`;
|
|
}
|
|
}
|
|
|
|
// ─── MY DOMAINS ─────────────────────────────────────────────
|
|
async function loadMyDomains() {
|
|
if (!walletAddress) return;
|
|
const content = $('#mydomains-content');
|
|
content.innerHTML = loadingHTML('LOADING YOUR DOMAINS...');
|
|
|
|
try {
|
|
const res = await fetch(`${SNS_API}/domains/${walletAddress}`);
|
|
const data = await res.json();
|
|
|
|
// Normalise to array
|
|
let domains = [];
|
|
if (Array.isArray(data)) {
|
|
domains = data;
|
|
} else if (data && typeof data === 'object') {
|
|
if (Array.isArray(data.result)) {
|
|
domains = data.result;
|
|
} else if (typeof data.result === 'string') {
|
|
domains = [data.result];
|
|
} else if (data.result && typeof data.result === 'object') {
|
|
domains = [data.result];
|
|
}
|
|
}
|
|
|
|
let favourite = null;
|
|
try {
|
|
const favRes = await fetch(`${SNS_API}/favourite-domain/${walletAddress}`);
|
|
const favData = await favRes.json();
|
|
if (favData.s === 'ok') favourite = favData.result || null;
|
|
} catch(e) {}
|
|
|
|
if (!domains || domains.length === 0) {
|
|
const regUrl = `${SNS_REG}?ref=${REFERRAL_WALLET}`;
|
|
content.innerHTML = `
|
|
<div class="sol-empty">
|
|
NO .SOL DOMAINS FOUND<br>
|
|
<a href="${regUrl}" target="_blank" rel="noopener" style="color:#14F195; font-size:0.7rem">Register one on SNS.id ↗</a>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
content.innerHTML = `
|
|
<div class="sol-stats-bar">
|
|
<div class="sol-stat">YOUR DOMAINS: <span class="sol-stat-value">${domains.length}</span></div>
|
|
${favourite ? `<div class="sol-stat">FAVOURITE: <span class="sol-stat-value">${favourite}.sol</span></div>` : ''}
|
|
</div>
|
|
<div class="sol-domains-grid">
|
|
${domains.map(d => {
|
|
const name = typeof d === 'string' ? d : (d.domain || d.name || String(d));
|
|
const isFav = favourite && name === favourite;
|
|
return `
|
|
<div class="sol-domain-card">
|
|
<div class="sol-domain-name">${esc(name)}<span class="sol-ext">.sol</span></div>
|
|
${isFav ? '<div class="sol-domain-fav">★ FAVOURITE</div>' : ''}
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
</div>
|
|
`;
|
|
} catch(err) {
|
|
content.innerHTML = errorHTML(`Network error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// ─── HELPERS ────────────────────────────────────────────────
|
|
function truncAddr(a) {
|
|
if (!a || a.length < 12) return a;
|
|
return a.slice(0, 6) + '...' + a.slice(-4);
|
|
}
|
|
|
|
function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function loadingHTML(msg) {
|
|
return `<div class="sol-loading"><div class="sol-loading-spinner"></div><div class="sol-loading-text">${msg}</div></div>`;
|
|
}
|
|
|
|
function errorHTML(msg) {
|
|
return `<div class="sol-error">ERROR // ${msg}</div>`;
|
|
}
|
|
|
|
// ─── REGISTRATION ───────────────────────────────────────────
|
|
|
|
function getEstimatedPrice(domain) {
|
|
const len = domain.length;
|
|
if (len <= 1) return { amount: '~750', currency: 'USDC', numeric: 750 };
|
|
if (len === 2) return { amount: '~700', currency: 'USDC', numeric: 700 };
|
|
if (len === 3) return { amount: '~640', currency: 'USDC', numeric: 640 };
|
|
if (len === 4) return { amount: '~160', currency: 'USDC', numeric: 160 };
|
|
return { amount: '~20', currency: 'USDC', numeric: 20 };
|
|
}
|
|
|
|
function createRegistrationModal() {
|
|
const modal = document.createElement('div');
|
|
modal.id = 'registration-modal';
|
|
modal.className = 'sol-modal hidden';
|
|
modal.innerHTML = `
|
|
<div class="sol-modal-backdrop"></div>
|
|
<div class="sol-modal-content sol-reg-modal-content">
|
|
<div class="sol-modal-header">
|
|
<span class="sol-modal-title">CONFIRM REGISTRATION</span>
|
|
<button class="sol-modal-close" id="reg-modal-close">✕</button>
|
|
</div>
|
|
<div class="sol-modal-body" id="reg-modal-body"></div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(modal);
|
|
modal.querySelector('.sol-modal-backdrop').addEventListener('click', closeRegistrationModal);
|
|
modal.querySelector('#reg-modal-close').addEventListener('click', closeRegistrationModal);
|
|
}
|
|
|
|
function initiateRegistration(domain) {
|
|
if (!walletAddress) {
|
|
pendingRegistrationDomain = domain;
|
|
openModal();
|
|
return;
|
|
}
|
|
showRegistrationModal(domain);
|
|
}
|
|
|
|
function checkPendingRegistration() {
|
|
if (pendingRegistrationDomain && walletAddress) {
|
|
const domain = pendingRegistrationDomain;
|
|
pendingRegistrationDomain = null;
|
|
setTimeout(() => showRegistrationModal(domain), 300);
|
|
}
|
|
}
|
|
|
|
function showRegistrationModal(domain) {
|
|
const modal = $('#registration-modal');
|
|
const body = $('#reg-modal-body');
|
|
const price = getEstimatedPrice(domain);
|
|
|
|
body.innerHTML = `
|
|
<div class="sol-reg-summary">
|
|
<div class="sol-reg-domain-display">
|
|
<span class="sol-reg-domain-name">${esc(domain)}</span><span class="sol-reg-domain-ext">.sol</span>
|
|
</div>
|
|
<div class="sol-reg-details">
|
|
<div class="sol-reg-row">
|
|
<span class="sol-reg-label">DOMAIN</span>
|
|
<span class="sol-reg-value">${esc(domain)}.sol</span>
|
|
</div>
|
|
<div class="sol-reg-row">
|
|
<span class="sol-reg-label">ESTIMATED COST</span>
|
|
<span class="sol-reg-value sol-reg-price">${price.amount} ${price.currency}</span>
|
|
</div>
|
|
<div class="sol-reg-row">
|
|
<span class="sol-reg-label">PAYMENT TOKEN</span>
|
|
<span class="sol-reg-value">USDC (Solana)</span>
|
|
</div>
|
|
<div class="sol-reg-row">
|
|
<span class="sol-reg-label">BUYER WALLET</span>
|
|
<span class="sol-reg-value">${truncAddr(walletAddress)}</span>
|
|
</div>
|
|
<div class="sol-reg-row">
|
|
<span class="sol-reg-label">STORAGE SPACE</span>
|
|
<span class="sol-reg-value">0 kB (minimum)</span>
|
|
</div>
|
|
</div>
|
|
<div class="sol-reg-warning" id="reg-warning">
|
|
⚠ THIS IS A REAL ON-CHAIN TRANSACTION. USDC WILL BE DEDUCTED FROM YOUR WALLET.
|
|
THE EXACT AMOUNT IS DETERMINED BY THE BONFIDA SNS SMART CONTRACT.
|
|
</div>
|
|
<div class="sol-reg-actions" id="reg-actions">
|
|
<button class="sol-reg-cancel-btn" id="reg-cancel">CANCEL</button>
|
|
<button class="sol-reg-confirm-btn" id="reg-confirm">CONFIRM & REGISTER</button>
|
|
</div>
|
|
</div>
|
|
<div class="sol-reg-status hidden" id="reg-status"></div>
|
|
`;
|
|
|
|
body.querySelector('#reg-cancel').addEventListener('click', closeRegistrationModal);
|
|
body.querySelector('#reg-confirm').addEventListener('click', () => executeRegistration(domain));
|
|
modal.classList.remove('hidden');
|
|
}
|
|
|
|
function closeRegistrationModal() {
|
|
const modal = $('#registration-modal');
|
|
if (modal) modal.classList.add('hidden');
|
|
}
|
|
|
|
function updateRegistrationStatus(state, message, extra) {
|
|
const statusEl = $('#reg-status');
|
|
if (!statusEl) return;
|
|
statusEl.classList.remove('hidden');
|
|
|
|
const actions = $('#reg-actions');
|
|
const warning = $('#reg-warning');
|
|
|
|
let icon, stateClass;
|
|
switch (state) {
|
|
case 'processing':
|
|
icon = '<div class="sol-loading-spinner"></div>';
|
|
stateClass = 'processing';
|
|
if (actions) actions.style.display = 'none';
|
|
if (warning) warning.style.display = 'none';
|
|
break;
|
|
case 'pending':
|
|
icon = '<div class="sol-loading-spinner"></div>';
|
|
stateClass = 'pending';
|
|
break;
|
|
case 'success':
|
|
icon = '<span class="sol-reg-status-icon success">✓</span>';
|
|
stateClass = 'success';
|
|
if (actions) actions.style.display = 'none';
|
|
if (warning) warning.style.display = 'none';
|
|
break;
|
|
case 'error':
|
|
icon = '<span class="sol-reg-status-icon error">✕</span>';
|
|
stateClass = 'error';
|
|
if (actions) { actions.style.display = 'flex'; }
|
|
if (warning) { warning.style.display = 'block'; }
|
|
break;
|
|
}
|
|
|
|
statusEl.innerHTML = `
|
|
<div class="sol-reg-status-card ${stateClass}">
|
|
<div class="sol-reg-status-header">
|
|
${icon}
|
|
<span class="sol-reg-status-text">${message}</span>
|
|
</div>
|
|
${extra || ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function executeRegistration(domain) {
|
|
if (!walletAddress || !window.solWallet?.provider) {
|
|
updateRegistrationStatus('error', 'WALLET NOT CONNECTED',
|
|
'<p class="sol-reg-error-detail">Please connect your wallet and try again.</p>');
|
|
return;
|
|
}
|
|
|
|
updateRegistrationStatus('processing', 'FETCHING TRANSACTION FROM SNS...');
|
|
|
|
try {
|
|
// 1. Fetch serialised transaction from SDK proxy
|
|
const params = new URLSearchParams({
|
|
buyer: walletAddress,
|
|
domain: domain,
|
|
space: '0',
|
|
serialize: 'true',
|
|
refKey: REFERRAL_WALLET,
|
|
mint: USDC_MINT
|
|
});
|
|
|
|
const apiUrl = `${SNS_REGISTER_API}/register?${params.toString()}`;
|
|
const res = await fetch(apiUrl);
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`SNS API returned HTTP ${res.status}`);
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
if (data.s !== 'ok' || !data.result) {
|
|
const errMsg = typeof data.result === 'string' ? data.result : (data.error || 'Unknown error from SNS API');
|
|
throw new Error(errMsg);
|
|
}
|
|
|
|
updateRegistrationStatus('processing', 'PREPARING TRANSACTION...');
|
|
|
|
// 2. Decode base64 transaction
|
|
const txBytes = base64ToUint8Array(data.result);
|
|
|
|
// 3. Deserialise — try VersionedTransaction first, fallback to legacy
|
|
let transaction;
|
|
let isVersioned = false;
|
|
|
|
try {
|
|
transaction = solanaWeb3.VersionedTransaction.deserialize(txBytes);
|
|
isVersioned = true;
|
|
} catch (e) {
|
|
try {
|
|
transaction = solanaWeb3.Transaction.from(txBytes);
|
|
isVersioned = false;
|
|
} catch (e2) {
|
|
throw new Error('Failed to deserialise transaction from SNS API');
|
|
}
|
|
}
|
|
|
|
// 4. Get fresh blockhash from Solana RPC
|
|
const connection = new solanaWeb3.Connection(SOLANA_RPC, 'confirmed');
|
|
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
|
|
|
|
// 5. Set fresh blockhash on transaction
|
|
if (isVersioned) {
|
|
transaction.message.recentBlockhash = blockhash;
|
|
} else {
|
|
transaction.recentBlockhash = blockhash;
|
|
}
|
|
|
|
updateRegistrationStatus('processing', 'AWAITING WALLET SIGNATURE...');
|
|
|
|
// 6. Sign and send via connected wallet
|
|
let signature;
|
|
|
|
if (window.solWallet && window.solWallet.isWalletStandard) {
|
|
signature = await signAndSendWalletStandard(transaction, isVersioned, connection);
|
|
} else {
|
|
signature = await signAndSendLegacy(transaction, isVersioned, connection);
|
|
}
|
|
|
|
updateRegistrationStatus('pending', 'TRANSACTION SUBMITTED — CONFIRMING...',
|
|
`<div class="sol-reg-tx-info">
|
|
<span class="sol-reg-label">TX SIGNATURE</span>
|
|
<a href="${SOLSCAN_TX}${signature}" target="_blank" rel="noopener" class="sol-reg-tx-link">${truncAddr(signature)} ↗</a>
|
|
</div>`);
|
|
|
|
// 7. Confirm transaction on-chain
|
|
const confirmation = await connection.confirmTransaction({
|
|
signature,
|
|
blockhash,
|
|
lastValidBlockHeight
|
|
}, 'confirmed');
|
|
|
|
if (confirmation.value.err) {
|
|
throw new Error('Transaction failed on-chain: ' + JSON.stringify(confirmation.value.err));
|
|
}
|
|
|
|
// 8. Success!
|
|
updateRegistrationStatus('success',
|
|
`${domain.toUpperCase()}.SOL REGISTERED SUCCESSFULLY!`,
|
|
`<div class="sol-reg-tx-info">
|
|
<span class="sol-reg-label">TRANSACTION</span>
|
|
<a href="${SOLSCAN_TX}${signature}" target="_blank" rel="noopener" class="sol-reg-tx-link">View on Solscan ↗</a>
|
|
</div>
|
|
<div class="sol-reg-tx-info">
|
|
<span class="sol-reg-label">DOMAIN</span>
|
|
<a href="${SNS_REG}${encodeURIComponent(domain)}" target="_blank" rel="noopener" class="sol-reg-tx-link">View on SNS.id ↗</a>
|
|
</div>
|
|
<div class="sol-reg-success-actions">
|
|
<button class="sol-reg-confirm-btn" onclick="closeRegistrationModal()">CLOSE</button>
|
|
</div>`);
|
|
|
|
} catch (err) {
|
|
console.error('Registration error:', err);
|
|
let userMessage = err.message || 'Unknown error';
|
|
|
|
// User-friendly error messages
|
|
if (/reject|denied|cancel|declined|disapproved/i.test(userMessage)) {
|
|
userMessage = 'Transaction was rejected by your wallet.';
|
|
} else if (/insufficient|not enough|0x1/i.test(userMessage)) {
|
|
userMessage = 'Insufficient USDC balance. Ensure you have enough USDC to cover the registration cost plus network fees.';
|
|
} else if (/already taken|already registered|already exists|registered/i.test(userMessage)) {
|
|
userMessage = 'This domain was just registered by someone else. Try a different name.';
|
|
} else if (/blockhash|expired|block height exceeded/i.test(userMessage)) {
|
|
userMessage = 'Transaction expired. Please try again.';
|
|
} else if (/network|fetch|failed to fetch|CORS/i.test(userMessage)) {
|
|
userMessage = 'Network error. Check your connection and try again.';
|
|
}
|
|
|
|
updateRegistrationStatus('error', 'REGISTRATION FAILED',
|
|
`<p class="sol-reg-error-detail">${esc(userMessage)}</p>
|
|
<div class="sol-reg-success-actions">
|
|
<button class="sol-reg-confirm-btn" id="reg-retry-btn">RETRY</button>
|
|
<button class="sol-reg-cancel-btn" onclick="closeRegistrationModal()">CLOSE</button>
|
|
</div>`);
|
|
|
|
// Bind retry after DOM update
|
|
const retryBtn = document.querySelector('#reg-retry-btn');
|
|
if (retryBtn) retryBtn.addEventListener('click', () => executeRegistration(domain));
|
|
}
|
|
}
|
|
|
|
async function signAndSendLegacy(transaction, isVersioned, connection) {
|
|
const provider = window.solWallet.provider;
|
|
|
|
// Try signAndSendTransaction first (Phantom, Solflare support this)
|
|
if (typeof provider.signAndSendTransaction === 'function') {
|
|
try {
|
|
const result = await provider.signAndSendTransaction(transaction, {
|
|
skipPreflight: false,
|
|
preflightCommitment: 'confirmed'
|
|
});
|
|
return result.signature || result;
|
|
} catch (e) {
|
|
if (/reject|denied|cancel|declined|disapproved/i.test(e.message || '')) throw e;
|
|
console.warn('signAndSendTransaction failed, falling back to signTransaction:', e.message);
|
|
}
|
|
}
|
|
|
|
// Fallback: signTransaction + manual send
|
|
if (typeof provider.signTransaction !== 'function') {
|
|
throw new Error('Wallet does not support transaction signing.');
|
|
}
|
|
const signed = await provider.signTransaction(transaction);
|
|
const rawTx = signed.serialize();
|
|
const signature = await connection.sendRawTransaction(rawTx, {
|
|
skipPreflight: false,
|
|
preflightCommitment: 'confirmed'
|
|
});
|
|
return signature;
|
|
}
|
|
|
|
async function signAndSendWalletStandard(transaction, isVersioned, connection) {
|
|
const wallet = window.solWallet.provider;
|
|
const account = wallet.accounts?.[0];
|
|
if (!account) throw new Error('No wallet account available.');
|
|
|
|
// Serialise the transaction for Wallet Standard
|
|
let txBytes;
|
|
if (isVersioned) {
|
|
txBytes = transaction.serialize();
|
|
} else {
|
|
txBytes = transaction.serialize({ requireAllSignatures: false, verifySignatures: false });
|
|
}
|
|
|
|
// Try signAndSendTransaction first
|
|
const signAndSendFeature = wallet.features?.['solana:signAndSendTransaction'];
|
|
if (signAndSendFeature) {
|
|
try {
|
|
const results = await signAndSendFeature.signAndSendTransaction({
|
|
account,
|
|
transaction: txBytes,
|
|
chain: 'solana:mainnet'
|
|
});
|
|
const result = Array.isArray(results) ? results[0] : results;
|
|
if (result.signature) {
|
|
if (typeof result.signature === 'string') return result.signature;
|
|
return uint8ArrayToBase58(new Uint8Array(result.signature));
|
|
}
|
|
return result;
|
|
} catch (e) {
|
|
if (/reject|denied|cancel|declined|disapproved/i.test(e.message || '')) throw e;
|
|
console.warn('WS signAndSendTransaction failed, falling back:', e.message);
|
|
}
|
|
}
|
|
|
|
// Fallback: signTransaction + manual send
|
|
const signFeature = wallet.features?.['solana:signTransaction'];
|
|
if (!signFeature) throw new Error('Wallet does not support Solana transaction signing.');
|
|
|
|
const results = await signFeature.signTransaction({
|
|
account,
|
|
transaction: txBytes,
|
|
chain: 'solana:mainnet'
|
|
});
|
|
const result = Array.isArray(results) ? results[0] : results;
|
|
const signedBytes = result.signedTransaction;
|
|
|
|
const signature = await connection.sendRawTransaction(signedBytes, {
|
|
skipPreflight: false,
|
|
preflightCommitment: 'confirmed'
|
|
});
|
|
return signature;
|
|
}
|
|
|
|
function base64ToUint8Array(base64) {
|
|
const binary = atob(base64);
|
|
const bytes = new Uint8Array(binary.length);
|
|
for (let i = 0; i < binary.length; i++) {
|
|
bytes[i] = binary.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
function uint8ArrayToBase58(bytes) {
|
|
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
let result = '';
|
|
let num = BigInt(0);
|
|
for (const b of bytes) num = num * 256n + BigInt(b);
|
|
while (num > 0n) {
|
|
result = ALPHABET[Number(num % 58n)] + result;
|
|
num = num / 58n;
|
|
}
|
|
for (const b of bytes) {
|
|
if (b === 0) result = '1' + result;
|
|
else break;
|
|
}
|
|
return result || '1';
|
|
}
|