diff --git a/website/solana.html b/website/solana.html new file mode 100644 index 0000000..6daa5ed --- /dev/null +++ b/website/solana.html @@ -0,0 +1,125 @@ + + + + + + Solana PoWH3D dApp + + + + + + + + + + +
+
+
+ +
Not connected
+
+
+
+

Configuration

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Register

+

Creates your on-chain user account and your token account (ATA) if missing.

+ +
+ +
+

Buy

+
+ + +
+ +
+ +
+

Sell

+
+ + +
+ +
+ +
+

Withdraw

+

Claims your accumulated dividends and referral rewards.

+ +
+ +
+

Status

+
+
+
+
+
+ +
+

Logs

+
+
+
+ + \ No newline at end of file diff --git a/website/solana.js b/website/solana.js new file mode 100644 index 0000000..93b9b0f --- /dev/null +++ b/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) 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 {} + }); + }); +})(); \ No newline at end of file