diff --git a/css/soldomains.css b/css/soldomains.css index 2305602..c5a50fb 100644 --- a/css/soldomains.css +++ b/css/soldomains.css @@ -598,3 +598,225 @@ border-top: 1px solid rgba(255, 255, 255, 0.05); margin-top: 0.25rem; } + +/* ─── Registration Modal ──────────────────────────────────── */ + +.sol-reg-modal-content { + max-width: 480px; +} + +.sol-reg-summary { + padding: 0.5rem; +} + +.sol-reg-domain-display { + text-align: center; + padding: 1.5rem 0 1.25rem; + border-bottom: 1px solid rgba(20, 241, 149, 0.15); + margin-bottom: 1rem; +} + +.sol-reg-domain-name { + font-family: 'JetBrains Mono', monospace; + font-size: 1.4rem; + color: #ffffff; + letter-spacing: 3px; +} + +.sol-reg-domain-ext { + font-family: 'JetBrains Mono', monospace; + font-size: 1.4rem; + color: #14F195; + letter-spacing: 3px; +} + +.sol-reg-details { + margin-bottom: 1rem; +} + +.sol-reg-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.55rem 0.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); +} + +.sol-reg-row:last-child { + border-bottom: none; +} + +.sol-reg-label { + font-family: 'JetBrains Mono', monospace; + font-size: 0.55rem; + color: rgba(255, 255, 255, 0.35); + letter-spacing: 2px; +} + +.sol-reg-value { + font-family: 'JetBrains Mono', monospace; + font-size: 0.65rem; + color: rgba(255, 255, 255, 0.8); + letter-spacing: 1px; +} + +.sol-reg-price { + color: #14F195; + font-size: 0.75rem; +} + +.sol-reg-warning { + background: rgba(255, 170, 0, 0.08); + border: 1px solid rgba(255, 170, 0, 0.2); + border-left: 3px solid rgba(255, 170, 0, 0.5); + padding: 0.75rem 1rem; + font-family: 'JetBrains Mono', monospace; + font-size: 0.5rem; + color: rgba(255, 170, 0, 0.85); + letter-spacing: 1px; + line-height: 1.6; + margin-bottom: 1rem; +} + +.sol-reg-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + padding-top: 0.5rem; +} + +.sol-reg-cancel-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.15); + color: rgba(255, 255, 255, 0.5); + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + letter-spacing: 2px; + padding: 0.65rem 1.5rem; + cursor: pointer; + transition: all 0.25s ease; +} + +.sol-reg-cancel-btn:hover { + border-color: rgba(255, 50, 50, 0.4); + color: rgba(255, 50, 50, 0.7); + background: rgba(255, 50, 50, 0.08); +} + +.sol-reg-confirm-btn { + background: linear-gradient(135deg, rgba(20, 241, 149, 0.25), rgba(168, 85, 247, 0.15)); + border: 1px solid rgba(20, 241, 149, 0.5); + color: #ffffff; + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + letter-spacing: 2px; + padding: 0.65rem 1.5rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.sol-reg-confirm-btn:hover { + background: linear-gradient(135deg, rgba(20, 241, 149, 0.4), rgba(168, 85, 247, 0.3)); + border-color: #14F195; + box-shadow: 0 0 20px rgba(20, 241, 149, 0.2); +} + +/* Registration Status */ +.sol-reg-status { + margin-top: 1rem; +} + +.sol-reg-status-card { + padding: 1rem; + border: 1px solid; + border-left-width: 3px; +} + +.sol-reg-status-card.processing { + border-color: rgba(20, 241, 149, 0.3); + border-left-color: rgba(20, 241, 149, 0.6); + background: rgba(20, 241, 149, 0.04); +} + +.sol-reg-status-card.pending { + border-color: rgba(255, 170, 0, 0.3); + border-left-color: rgba(255, 170, 0, 0.6); + background: rgba(255, 170, 0, 0.04); +} + +.sol-reg-status-card.success { + border-color: rgba(20, 241, 149, 0.4); + border-left-color: #14F195; + background: rgba(20, 241, 149, 0.06); +} + +.sol-reg-status-card.error { + border-color: rgba(255, 50, 50, 0.3); + border-left-color: rgba(255, 50, 50, 0.6); + background: rgba(255, 50, 50, 0.04); +} + +.sol-reg-status-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.sol-reg-status-text { + font-family: 'JetBrains Mono', monospace; + font-size: 0.65rem; + color: #ffffff; + letter-spacing: 2px; +} + +.sol-reg-status-icon { + font-size: 1.1rem; + width: 24px; + text-align: center; +} + +.sol-reg-status-icon.success { + color: #14F195; +} + +.sol-reg-status-icon.error { + color: rgba(255, 50, 50, 0.8); +} + +.sol-reg-tx-info { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.4rem 0; +} + +.sol-reg-tx-link { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + color: #14F195; + text-decoration: none; + letter-spacing: 1px; +} + +.sol-reg-tx-link:hover { + text-decoration: underline; +} + +.sol-reg-error-detail { + font-family: 'JetBrains Mono', monospace; + font-size: 0.6rem; + color: rgba(255, 50, 50, 0.75); + letter-spacing: 0.5px; + line-height: 1.6; + margin: 0.5rem 0; +} + +.sol-reg-success-actions { + display: flex; + gap: 0.75rem; + justify-content: flex-end; + margin-top: 0.75rem; + padding-top: 0.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} diff --git a/js/soldomains.js b/js/soldomains.js index db5f7a2..49748cf 100644 --- a/js/soldomains.js +++ b/js/soldomains.js @@ -2,11 +2,16 @@ 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); @@ -16,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => { initTabs(); initSearch(); initWallet(); + createRegistrationModal(); }); // ─── TABS ─────────────────────────────────────────────────── @@ -121,15 +127,7 @@ async function showTakenDomain(domain, owner) { function showAvailableDomain(domain) { const results = $('#search-results'); - const len = domain.length; - let priceEstimate = ''; - if (len <= 1) priceEstimate = '~750 USDC'; - else if (len === 2) priceEstimate = '~700 USDC'; - else if (len === 3) priceEstimate = '~640 USDC'; - else if (len === 4) priceEstimate = '~160 USDC'; - else priceEstimate = '~20 USDC'; - - const regUrl = `${SNS_REG}${encodeURIComponent(domain)}?ref=${REFERRAL_WALLET}`; + const price = getEstimatedPrice(domain); results.innerHTML = `
@@ -141,13 +139,13 @@ function showAvailableDomain(domain) {
LENGTH -
${len} character${len !== 1 ? 's' : ''}
+
${domain.length} character${domain.length !== 1 ? 's' : ''}
ESTIMATED COST
- ${priceEstimate.split(' ')[0].replace('~','')} - ${priceEstimate.split(' ')[1] || 'USDC'} + ${price.amount.replace('~','')} + ${price.currency} (estimated)
@@ -155,13 +153,22 @@ function showAvailableDomain(domain) {
REGISTRATION -
Purchase your .sol domain
+
Register directly from your wallet
- REGISTER ${esc(domain.toUpperCase())}.SOL ↗ +
+ PAYMENT +
USDC on Solana
+
+
`; + + const regBtn = results.querySelector('#register-domain-btn'); + if (regBtn) { + regBtn.addEventListener('click', () => initiateRegistration(domain)); + } } // ─── REVERSE LOOKUP ───────────────────────────────────────── @@ -496,6 +503,7 @@ async function connectWallet(wallet) { walletAddress = accounts[0].address; connectedProvider = wallet; updateWalletUI(); + checkPendingRegistration(); } else { throw new Error('No accounts returned'); } @@ -505,6 +513,7 @@ async function connectWallet(wallet) { walletAddress = resp.publicKey.toString(); connectedProvider = wallet; updateWalletUI(); + checkPendingRegistration(); } } catch(err) { console.error('Wallet connection failed:', err); @@ -627,3 +636,396 @@ function loadingHTML(msg) { function errorHTML(msg) { return `
ERROR // ${msg}
`; } + +// ─── 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 = ` +
+
+
+ CONFIRM REGISTRATION + +
+
+
+ `; + 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 = ` +
+
+ ${esc(domain)}.sol +
+
+
+ DOMAIN + ${esc(domain)}.sol +
+
+ ESTIMATED COST + ${price.amount} ${price.currency} +
+
+ PAYMENT TOKEN + USDC (Solana) +
+
+ BUYER WALLET + ${truncAddr(walletAddress)} +
+
+ STORAGE SPACE + 0 kB (minimum) +
+
+
+ ⚠ 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. +
+
+ + +
+
+ + `; + + 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 = '
'; + stateClass = 'processing'; + if (actions) actions.style.display = 'none'; + if (warning) warning.style.display = 'none'; + break; + case 'pending': + icon = '
'; + stateClass = 'pending'; + break; + case 'success': + icon = ''; + stateClass = 'success'; + if (actions) actions.style.display = 'none'; + if (warning) warning.style.display = 'none'; + break; + case 'error': + icon = ''; + stateClass = 'error'; + if (actions) { actions.style.display = 'flex'; } + if (warning) { warning.style.display = 'block'; } + break; + } + + statusEl.innerHTML = ` +
+
+ ${icon} + ${message} +
+ ${extra || ''} +
+ `; +} + +async function executeRegistration(domain) { + if (!walletAddress || !connectedProvider) { + updateRegistrationStatus('error', 'WALLET NOT CONNECTED', + '

Please connect your wallet and try again.

'); + 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 (connectedProvider.isWalletStandard) { + signature = await signAndSendWalletStandard(transaction, isVersioned, connection); + } else { + signature = await signAndSendLegacy(transaction, isVersioned, connection); + } + + updateRegistrationStatus('pending', 'TRANSACTION SUBMITTED — CONFIRMING...', + `
+ TX SIGNATURE + ${truncAddr(signature)} ↗ +
`); + + // 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!`, + `
+ TRANSACTION + View on Solscan ↗ +
+
+ DOMAIN + View on SNS.id ↗ +
+
+ +
`); + + } 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', + `

${esc(userMessage)}

+
+ + +
`); + + // 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 = connectedProvider.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 = connectedProvider.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'; +} diff --git a/soldomains/index.html b/soldomains/index.html index 5d14c45..fd11727 100644 --- a/soldomains/index.html +++ b/soldomains/index.html @@ -115,6 +115,7 @@ +