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