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