feat: direct on-chain .sol domain registration with referral
This commit is contained in:
parent
8d4dec5161
commit
77f2e526f4
3 changed files with 639 additions and 14 deletions
|
|
@ -598,3 +598,225 @@
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
margin-top: 0.25rem;
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
430
js/soldomains.js
430
js/soldomains.js
|
|
@ -2,11 +2,16 @@
|
||||||
|
|
||||||
const SNS_API = 'https://sns-sdk-proxy.bonfida.workers.dev';
|
const SNS_API = 'https://sns-sdk-proxy.bonfida.workers.dev';
|
||||||
const SNS_REG = 'https://www.sns.id/domain/';
|
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 = 'https://solscan.io/account/';
|
||||||
|
const SOLSCAN_TX = 'https://solscan.io/tx/';
|
||||||
const REFERRAL_WALLET = '9NuiHh5wgRPx69BFGP1ZR8kHiBENGoJrXs5GpZzKAyn8';
|
const REFERRAL_WALLET = '9NuiHh5wgRPx69BFGP1ZR8kHiBENGoJrXs5GpZzKAyn8';
|
||||||
|
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
||||||
|
const SOLANA_RPC = 'https://api.mainnet-beta.solana.com';
|
||||||
|
|
||||||
let walletAddress = null;
|
let walletAddress = null;
|
||||||
let currentTab = 'search';
|
let currentTab = 'search';
|
||||||
|
let pendingRegistrationDomain = null;
|
||||||
|
|
||||||
// ─── DOM ────────────────────────────────────────────────────
|
// ─── DOM ────────────────────────────────────────────────────
|
||||||
const $ = s => document.querySelector(s);
|
const $ = s => document.querySelector(s);
|
||||||
|
|
@ -16,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
initTabs();
|
initTabs();
|
||||||
initSearch();
|
initSearch();
|
||||||
initWallet();
|
initWallet();
|
||||||
|
createRegistrationModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── TABS ───────────────────────────────────────────────────
|
// ─── TABS ───────────────────────────────────────────────────
|
||||||
|
|
@ -121,15 +127,7 @@ async function showTakenDomain(domain, owner) {
|
||||||
|
|
||||||
function showAvailableDomain(domain) {
|
function showAvailableDomain(domain) {
|
||||||
const results = $('#search-results');
|
const results = $('#search-results');
|
||||||
const len = domain.length;
|
const price = getEstimatedPrice(domain);
|
||||||
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}`;
|
|
||||||
|
|
||||||
results.innerHTML = `
|
results.innerHTML = `
|
||||||
<div class="sol-result-card">
|
<div class="sol-result-card">
|
||||||
|
|
@ -141,13 +139,13 @@ function showAvailableDomain(domain) {
|
||||||
<div>
|
<div>
|
||||||
<div class="sol-result-field">
|
<div class="sol-result-field">
|
||||||
<span class="sol-result-label">LENGTH</span>
|
<span class="sol-result-label">LENGTH</span>
|
||||||
<div class="sol-result-value">${len} character${len !== 1 ? 's' : ''}</div>
|
<div class="sol-result-value">${domain.length} character${domain.length !== 1 ? 's' : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sol-result-field">
|
<div class="sol-result-field">
|
||||||
<span class="sol-result-label">ESTIMATED COST</span>
|
<span class="sol-result-label">ESTIMATED COST</span>
|
||||||
<div class="sol-price">
|
<div class="sol-price">
|
||||||
<span class="sol-price-amount">${priceEstimate.split(' ')[0].replace('~','')}</span>
|
<span class="sol-price-amount">${price.amount.replace('~','')}</span>
|
||||||
<span class="sol-price-currency">${priceEstimate.split(' ')[1] || 'USDC'}</span>
|
<span class="sol-price-currency">${price.currency}</span>
|
||||||
<span class="sol-price-usd">(estimated)</span>
|
<span class="sol-price-usd">(estimated)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -155,13 +153,22 @@ function showAvailableDomain(domain) {
|
||||||
<div>
|
<div>
|
||||||
<div class="sol-result-field">
|
<div class="sol-result-field">
|
||||||
<span class="sol-result-label">REGISTRATION</span>
|
<span class="sol-result-label">REGISTRATION</span>
|
||||||
<div class="sol-result-value">Purchase your .sol domain</div>
|
<div class="sol-result-value">Register directly from your wallet</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="${regUrl}" target="_blank" rel="noopener" class="sol-register-btn">REGISTER ${esc(domain.toUpperCase())}.SOL ↗</a>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const regBtn = results.querySelector('#register-domain-btn');
|
||||||
|
if (regBtn) {
|
||||||
|
regBtn.addEventListener('click', () => initiateRegistration(domain));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── REVERSE LOOKUP ─────────────────────────────────────────
|
// ─── REVERSE LOOKUP ─────────────────────────────────────────
|
||||||
|
|
@ -496,6 +503,7 @@ async function connectWallet(wallet) {
|
||||||
walletAddress = accounts[0].address;
|
walletAddress = accounts[0].address;
|
||||||
connectedProvider = wallet;
|
connectedProvider = wallet;
|
||||||
updateWalletUI();
|
updateWalletUI();
|
||||||
|
checkPendingRegistration();
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No accounts returned');
|
throw new Error('No accounts returned');
|
||||||
}
|
}
|
||||||
|
|
@ -505,6 +513,7 @@ async function connectWallet(wallet) {
|
||||||
walletAddress = resp.publicKey.toString();
|
walletAddress = resp.publicKey.toString();
|
||||||
connectedProvider = wallet;
|
connectedProvider = wallet;
|
||||||
updateWalletUI();
|
updateWalletUI();
|
||||||
|
checkPendingRegistration();
|
||||||
}
|
}
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error('Wallet connection failed:', err);
|
console.error('Wallet connection failed:', err);
|
||||||
|
|
@ -627,3 +636,396 @@ function loadingHTML(msg) {
|
||||||
function errorHTML(msg) {
|
function errorHTML(msg) {
|
||||||
return `<div class="sol-error">ERROR // ${msg}</div>`;
|
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 || !connectedProvider) {
|
||||||
|
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 (connectedProvider.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 = 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';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@
|
||||||
|
|
||||||
<script src="/js/nav.js"></script>
|
<script src="/js/nav.js"></script>
|
||||||
<script src="/js/clock.js"></script>
|
<script src="/js/clock.js"></script>
|
||||||
|
<script src="https://unpkg.com/@solana/web3.js@1.98.0/lib/index.iife.min.js"></script>
|
||||||
<script src="/js/soldomains.js"></script>
|
<script src="/js/soldomains.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue