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) {
`;
+
+ 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 = `
+
+
+ `;
+ 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 = `
+
+
+ ${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...',
+ ``);
+
+ // 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!`,
+ `
+
+
+
+
`);
+
+ } 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 @@
+