2 changed files with 437 additions and 0 deletions
@ -0,0 +1,125 @@
@@ -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> |
||||
@ -0,0 +1,312 @@
@@ -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…
Reference in new issue