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);
|
||||
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_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 = `
|
||||
<div class="sol-result-card">
|
||||
|
|
@ -141,13 +139,13 @@ function showAvailableDomain(domain) {
|
|||
<div>
|
||||
<div class="sol-result-field">
|
||||
<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 class="sol-result-field">
|
||||
<span class="sol-result-label">ESTIMATED COST</span>
|
||||
<div class="sol-price">
|
||||
<span class="sol-price-amount">${priceEstimate.split(' ')[0].replace('~','')}</span>
|
||||
<span class="sol-price-currency">${priceEstimate.split(' ')[1] || 'USDC'}</span>
|
||||
<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>
|
||||
|
|
@ -155,13 +153,22 @@ function showAvailableDomain(domain) {
|
|||
<div>
|
||||
<div class="sol-result-field">
|
||||
<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>
|
||||
<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>
|
||||
`;
|
||||
|
||||
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 `<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/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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue