You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

312 lines
12 KiB

/* 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 {}
});
});
})();