Browse Source

feat(dapp): Add Solana dApp page (wallet connect + buy/sell/withdraw) and wire analytics

master
crappyrules 3 months ago
parent
commit
16981d5d34
  1. 125
      website/solana.html
  2. 312
      website/solana.js

125
website/solana.html

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solana PoWH3D dApp</title>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script defer src="https://analytics.temple-os.com/script.js" data-website-id="92b291cb-15b7-423b-863a-129d4688aaf3"></script>
<script src="https://unpkg.com/@solana/web3.js@1.95.3/lib/index.iife.min.js"></script>
<script src="solana.js" defer></script>
<style>
.dapp-container { max-width: 1100px; margin: 2rem auto; padding: 1rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1rem; }
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 1rem; box-shadow: 0 2px 6px rgba(0,0,0,0.04); }
.card h3 { margin: 0 0 0.5rem 0; }
.row { display: flex; gap: 0.5rem; align-items: center; margin: 0.5rem 0; }
.row label { min-width: 180px; color: #374151; font-weight: 500; }
.row input, .row select { flex: 1; padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 6px; }
.btn { background: #4f46e5; border: none; color: #fff; padding: 0.6rem 1rem; border-radius: 8px; cursor: pointer; }
.btn.secondary { background: #10b981; }
.btn.danger { background: #ef4444; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.muted { color: #6b7280; font-size: 0.9rem; }
code.inline { background: #f3f4f6; padding: 0.1rem 0.3rem; border-radius: 4px; }
.log { background: #0b1020; color: #d1d5db; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; padding: 0.75rem; border-radius: 8px; height: 220px; overflow: auto; }
.ok { color: #10b981; }
.warn { color: #f59e0b; }
.err { color: #ef4444; }
</style>
</head>
<body>
<nav class="navbar">
<div class="nav-container">
<div class="nav-logo"><h2>Solana PoWH3D dApp</h2></div>
<ul class="nav-menu">
<li><a href="/">Home</a></li>
<li><a href="/solana.html">Solana dApp</a></li>
</ul>
</div>
</nav>
<div class="dapp-container">
<div class="card">
<div class="row">
<button id="connectBtn" class="btn">Connect Phantom</button>
<div id="walletInfo" class="muted">Not connected</div>
</div>
<div class="grid">
<div class="card">
<h3>Configuration</h3>
<div class="row">
<label>Cluster</label>
<select id="cluster">
<option value="https://api.devnet.solana.com">Devnet</option>
<option value="https://api.mainnet-beta.solana.com">Mainnet</option>
</select>
</div>
<div class="row">
<label>Program ID</label>
<input id="programId" value="9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM" />
</div>
<div class="row">
<label>Mint (Token)</label>
<input id="mint" placeholder="Paste the mint address here" />
</div>
<div class="row">
<label>Referrer (optional)</label>
<input id="referrer" placeholder="Referrer wallet address" />
</div>
<div class="row">
<label>Derived Powh PDA</label>
<input id="powhPda" readonly />
</div>
<div class="row">
<label>Derived User PDA</label>
<input id="userPda" readonly />
</div>
</div>
<div class="card">
<h3>Register</h3>
<p class="muted">Creates your on-chain user account and your token account (ATA) if missing.</p>
<button id="registerBtn" class="btn">Register User</button>
</div>
<div class="card">
<h3>Buy</h3>
<div class="row">
<label>SOL amount</label>
<input id="buyAmount" type="number" step="0.001" value="0.1" />
</div>
<button id="buyBtn" class="btn secondary">Buy Tokens</button>
</div>
<div class="card">
<h3>Sell</h3>
<div class="row">
<label>Tokens to sell (raw, 9dp)</label>
<input id="sellAmount" type="number" step="1" value="100000000" />
</div>
<button id="sellBtn" class="btn danger">Sell Tokens</button>
</div>
<div class="card">
<h3>Withdraw</h3>
<p class="muted">Claims your accumulated dividends and referral rewards.</p>
<button id="withdrawBtn" class="btn">Withdraw Dividends</button>
</div>
<div class="card">
<h3>Status</h3>
<div class="row"><label>Wallet</label><input id="walletAddr" readonly /></div>
<div class="row"><label>ATA</label><input id="ataAddr" readonly /></div>
</div>
</div>
</div>
<div class="card">
<h3>Logs</h3>
<div id="log" class="log"></div>
</div>
</div>
</body>
</html>

312
website/solana.js

@ -0,0 +1,312 @@
/* global solanaWeb3 */
(() => {
const { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction } = solanaWeb3;
// CONSTANTS
const TOKEN_PROGRAM_ID = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
// DOM helpers
const $ = (id) => document.getElementById(id);
const logEl = () => $('log');
const log = (msg, cls = '') => {
const el = logEl();
if (!el) return;
const line = document.createElement('div');
if (cls) line.className = cls;
line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
el.prepend(line);
};
// Buffer helpers
function le64(n) {
const b = new ArrayBuffer(8);
new DataView(b).setBigUint64(0, BigInt(n), true);
return new Uint8Array(b);
}
async function anchorDiscriminator(name) {
const data = new TextEncoder().encode(`global:${name}`);
const hash = await crypto.subtle.digest('SHA-256', data);
return new Uint8Array(hash).slice(0, 8);
}
// PDA helpers
async function findPdaPowh(programId) {
const [pda] = await PublicKey.findProgramAddress([new TextEncoder().encode('powh')], programId);
return pda;
}
async function findPdaUser(programId, owner) {
const [pda] = await PublicKey.findProgramAddress([new TextEncoder().encode('user'), owner.toBuffer()], programId);
return pda;
}
async function getAssociatedTokenAddress(mint, owner) {
const [addr] = await PublicKey.findProgramAddress(
[owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
ASSOCIATED_TOKEN_PROGRAM_ID
);
return addr;
}
function createATAInstruction(payer, ata, owner, mint) {
const keys = [
{ pubkey: payer, isSigner: true, isWritable: true },
{ pubkey: ata, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: false, isWritable: false },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: solanaWeb3.SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
];
const data = new Uint8Array([]);
return new TransactionInstruction({ keys, programId: ASSOCIATED_TOKEN_PROGRAM_ID, data });
}
// Build Anchor instruction with discriminators
async function buildIx(name, argsBytes) {
const disc = await anchorDiscriminator(name);
const data = new Uint8Array(disc.length + argsBytes.length);
data.set(disc, 0);
data.set(argsBytes, disc.length);
return data;
}
async function ensureWallet() {
if (!window.solana || !window.solana.isPhantom) {
throw new Error('Phantom wallet not found. Please install Phantom.');
}
const resp = await window.solana.connect();
return new PublicKey(resp.publicKey.toString());
}
function getConfig() {
const programId = new PublicKey($('programId').value.trim());
const mintStr = $('mint').value.trim();
if (!mintStr) throw new Error('Please set the Mint address.');
const mint = new PublicKey(mintStr);
const cluster = $('cluster').value;
return { programId, mint, cluster };
}
async function refreshDerived(pubkey) {
try {
const { programId } = getConfig();
const powhPda = await findPdaPowh(programId);
const userPda = await findPdaUser(programId, pubkey);
$('powhPda').value = powhPda.toBase58();
$('userPda').value = userPda.toBase58();
log(`Derived PDAs updated.`, 'ok');
} catch (e) { log(`PDA derivation failed: ${e.message}`, 'err'); }
}
async function connectWallet() {
try {
const pubkey = await ensureWallet();
$('walletInfo').textContent = `Connected: ${pubkey.toBase58()}`;
$('walletAddr').value = pubkey.toBase58();
await refreshDerived(pubkey);
} catch (e) {
log(e.message, 'err');
}
}
async function withConn(fn) {
const { cluster } = getConfig();
const conn = new Connection(cluster, 'confirmed');
return fn(conn);
}
async function ensureAta(conn, owner, mint) {
const ata = await getAssociatedTokenAddress(mint, owner);
const info = await conn.getAccountInfo(ata);
if (!info) {
log('ATA not found, creating...');
return { ata, createIx: createATAInstruction(owner, ata, owner, mint) };
}
return { ata };
}
async function handleRegister() {
try {
const { programId, mint } = getConfig();
const owner = await ensureWallet();
await withConn(async (conn) => {
const powhPda = await findPdaPowh(programId);
const userPda = await findPdaUser(programId, owner);
const { ata, createIx } = await ensureAta(conn, owner, mint);
// Build register_user(referrer: Option<Pubkey>) instruction
const refStr = $('referrer').value.trim();
const hasRef = !!refStr;
let args = new Uint8Array([hasRef ? 1 : 0]);
if (hasRef) {
const ref = new PublicKey(refStr);
args = new Uint8Array([...args, ...ref.toBytes()]);
}
const data = await buildIx('register_user', args);
const keys = [
{ pubkey: userPda, isSigner: false, isWritable: true },
{ pubkey: ata, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: owner, isSigner: true, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
{ pubkey: solanaWeb3.SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
];
const ix = new TransactionInstruction({ keys, programId, data });
const tx = new Transaction();
if (createIx) tx.add(createIx);
tx.add(ix);
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash('confirmed');
tx.recentBlockhash = blockhash;
tx.feePayer = owner;
const signed = await window.solana.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize(), { skipPreflight: false });
await conn.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed');
$('ataAddr').value = ata.toBase58();
log(`✅ Registered. Tx: ${sig}`, 'ok');
});
} catch (e) {
log(`Register failed: ${e.message}`, 'err');
}
}
async function handleBuy() {
try {
const { programId, mint } = getConfig();
const owner = await ensureWallet();
const solAmount = parseFloat($('buyAmount').value || '0');
const lamports = Math.floor(solAmount * 1_000_000_000);
if (lamports <= 0) throw new Error('Invalid SOL amount');
await withConn(async (conn) => {
const powhPda = await findPdaPowh(programId);
const userPda = await findPdaUser(programId, owner);
const { ata, createIx } = await ensureAta(conn, owner, mint);
// buy(lamports_amount: u64)
const data = await buildIx('buy', le64(lamports));
const refStr = $('referrer').value.trim();
const keys = [
{ pubkey: powhPda, isSigner: false, isWritable: true },
{ pubkey: userPda, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: true },
{ pubkey: ata, isSigner: false, isWritable: true },
// Optional accounts (pass if referrer set, else omit) - our program currently expects them; include safely when provided
...(refStr ? [{ pubkey: await getAssociatedTokenAddress(mint, new PublicKey(refStr)), isSigner: false, isWritable: false }] : []),
...(refStr ? [{ pubkey: await findPdaUser(programId, new PublicKey(refStr)), isSigner: false, isWritable: false }] : []),
{ pubkey: owner, isSigner: true, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
];
const ix = new TransactionInstruction({ keys, programId, data });
const tx = new Transaction();
if (createIx) tx.add(createIx);
tx.add(ix);
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash('confirmed');
tx.recentBlockhash = blockhash;
tx.feePayer = owner;
const signed = await window.solana.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize(), { skipPreflight: false });
await conn.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed');
log(`✅ Buy successful. Tx: ${sig}`, 'ok');
});
} catch (e) {
log(`Buy failed: ${e.message}`, 'err');
}
}
async function handleSell() {
try {
const { programId, mint } = getConfig();
const owner = await ensureWallet();
const tokensRaw = BigInt($('sellAmount').value || '0');
if (tokensRaw <= 0n) throw new Error('Invalid token amount');
await withConn(async (conn) => {
const powhPda = await findPdaPowh(programId);
const userPda = await findPdaUser(programId, owner);
const { ata } = await ensureAta(conn, owner, mint);
const data = await buildIx('sell', le64(tokensRaw));
const keys = [
{ pubkey: powhPda, isSigner: false, isWritable: true },
{ pubkey: userPda, isSigner: false, isWritable: true },
{ pubkey: mint, isSigner: false, isWritable: true },
{ pubkey: ata, isSigner: false, isWritable: true },
{ pubkey: owner, isSigner: true, isWritable: true },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
];
const ix = new TransactionInstruction({ keys, programId, data });
const tx = new Transaction().add(ix);
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash('confirmed');
tx.recentBlockhash = blockhash;
tx.feePayer = owner;
const signed = await window.solana.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize(), { skipPreflight: false });
await conn.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed');
log(`✅ Sell successful. Tx: ${sig}`, 'ok');
});
} catch (e) {
log(`Sell failed: ${e.message}`, 'err');
}
}
async function handleWithdraw() {
try {
const { programId, mint } = getConfig();
const owner = await ensureWallet();
await withConn(async (conn) => {
const powhPda = await findPdaPowh(programId);
const userPda = await findPdaUser(programId, owner);
const ata = await getAssociatedTokenAddress(mint, owner);
const data = await buildIx('withdraw', new Uint8Array([]));
const keys = [
{ pubkey: powhPda, isSigner: false, isWritable: true },
{ pubkey: userPda, isSigner: false, isWritable: true },
{ pubkey: ata, isSigner: false, isWritable: false },
{ pubkey: mint, isSigner: false, isWritable: false },
{ pubkey: owner, isSigner: true, isWritable: true },
];
const ix = new TransactionInstruction({ keys, programId, data });
const tx = new Transaction().add(ix);
const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash('confirmed');
tx.recentBlockhash = blockhash;
tx.feePayer = owner;
const signed = await window.solana.signTransaction(tx);
const sig = await conn.sendRawTransaction(signed.serialize(), { skipPreflight: false });
await conn.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }, 'confirmed');
log(`✅ Withdraw successful. Tx: ${sig}`, 'ok');
});
} catch (e) {
log(`Withdraw failed: ${e.message}`, 'err');
}
}
// Wire up events
window.addEventListener('DOMContentLoaded', () => {
$('connectBtn').addEventListener('click', connectWallet);
$('registerBtn').addEventListener('click', handleRegister);
$('buyBtn').addEventListener('click', handleBuy);
$('sellBtn').addEventListener('click', handleSell);
$('withdrawBtn').addEventListener('click', handleWithdraw);
$('cluster').addEventListener('change', () => log(`Cluster set to ${$('cluster').value}`, 'warn'));
$('programId').addEventListener('change', async () => {
try { await refreshDerived(new PublicKey($('walletAddr').value)); } catch {}
});
});
})();
Loading…
Cancel
Save