import { Connection, Keypair, PublicKey, ComputeBudgetProgram, TransactionMessage, VersionedTransaction, AddressLookupTableAccount } from '@solana/web3.js'; import {createPumpInstruction} from './pump-instruction.mjs'; import {createFeeInstructions} from './fee-instructions.mjs'; import {LAUNCH_LOOKUP} from './launch-lookup.mjs'; import {PLATFORM_FEE_WALLET,FEE_PROGRAM} from './fee-policy.mjs'; import { PUMP_PROGRAM, base58Encode, validateCoin, validatePublicUrl } from './shared.mjs'; export const endpoints = {devnet:'/api/rpc/devnet','mainnet-beta':'/api/rpc/mainnet-beta'}; function endpointFor(cluster,rpc){ if(!Object.hasOwn(endpoints,cluster))throw new Error('Unsupported network.'); const endpoint=rpc||new URL(endpoints[cluster],globalThis.location?.origin||'https://example.invalid').href; const parsed=new URL(endpoint);if(parsed.protocol!=='https:'&&!(parsed.protocol==='http:'&&parsed.hostname==='127.0.0.1'&&!rpc)||parsed.username||parsed.password)throw new Error('RPC endpoint must use HTTPS without embedded credentials.'); return endpoint; } const connectionOptions={commitment:'confirmed',fetch:(input,init)=>fetch(input,{...init,signal:AbortSignal.any([init?.signal,AbortSignal.timeout(20000)].filter(Boolean))})}; // HTTP polling works with the same-origin relay and avoids a WebSocket dependency. export async function confirmLaunch(connection,receipt,{timeoutMs=75000,now=Date.now,wait=ms=>new Promise(resolve=>setTimeout(resolve,ms))}={}){ const deadline=now()+timeoutMs; while(now()BigInt(receipt.lastValidBlockHeight)){const error=new Error('The launch transaction expired without confirmation.');error.receipt={...receipt,state:'expired'};throw error;} await wait(2000); } throw new Error('Confirmation timed out. Check the saved receipt before starting another launch.'); } // Solana's canonical cluster genesis hashes; verify a custom RPC before requesting a signature. const genesis = {devnet:'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG','mainnet-beta':'5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d'}; export async function buildLaunchInstruction({wallet,mint,name,symbol,uri,holderReward}) { validateCoin({name,symbol,description:''}); validatePublicUrl(uri); const instruction = await createPumpInstruction({mint,name,symbol,uri,user:wallet,holderReward:!!holderReward}); if (instruction.programId.toBase58() !== PUMP_PROGRAM) throw new Error('Unexpected coin creation program.'); return instruction; } export async function deployCoin({wallet,provider,name,symbol,uri,holderReward,cluster,rpc,onStep,onEstimate,onSubmitted,connectionFactory=(endpoint,options)=>new Connection(endpoint,options)}) { const connection = connectionFactory(endpointFor(cluster,rpc),connectionOptions); if (await connection.getGenesisHash() !== genesis[cluster]) throw new Error('RPC network does not match the selected network.'); const program = await connection.getAccountInfo(new PublicKey(PUMP_PROGRAM)); if (!program?.executable) throw new Error('Pump.fun’s program is unavailable on this network. Choose a supported RPC or network.'); const payer = new PublicKey(wallet); const mint = await Keypair.generate(); if(cluster==='mainnet-beta'&&holderReward)throw new Error('Holder rewards are unavailable. Each coin uses the creator/platform fee split.'); const create = await buildLaunchInstruction({wallet:payer,mint:mint.publicKey,name,symbol,uri,holderReward}); let sharing=null,lookups=[]; if(cluster==='mainnet-beta'){ const feeProgram=await connection.getAccountInfo(new PublicKey(FEE_PROGRAM)); if(!feeProgram?.executable)throw new Error('Pump.fun fee sharing is temporarily unavailable.'); sharing=await createFeeInstructions({creator:payer,mint:mint.publicKey}); const lookupKey=new PublicKey(LAUNCH_LOOKUP.address),lookupAccount=await connection.getAccountInfo(lookupKey); if(!lookupAccount||lookupAccount.owner.toBase58()!=='AddressLookupTab1e1111111111111111111111111')throw new Error('The launch lookup table is unavailable. Please retry later.'); const state=AddressLookupTableAccount.deserialize(lookupAccount.data); if(state.deactivationSlot!==18446744073709551615n||LAUNCH_LOOKUP.addresses.some((address,i)=>state.addresses[i]?.toBase58()!==address))throw new Error('The launch lookup table could not be verified. Please retry later.'); // Ignore later appended entries: browser and relay use the same verified snapshot. lookups=[new AddressLookupTableAccount({key:lookupKey,state:{...state,addresses:state.addresses.slice(0,LAUNCH_LOOKUP.addresses.length)}})]; } let priority=10000; try{const fees=await connection.getRecentPrioritizationFees({lockedWritableAccounts:create.keys.filter(k=>k.isWritable).map(k=>k.pubkey)});const values=fees.map(f=>Number(f.prioritizationFee)).filter(n=>Number.isFinite(n)&&n>=0).sort((a,b)=>a-b);if(values.length)priority=Math.max(10000,Math.min(1000000,Math.ceil(values[Math.floor(values.length*.75)])));}catch{} const recent = await connection.getLatestBlockhash('confirmed'); const message = new TransactionMessage({payerKey:payer,recentBlockhash:recent.blockhash,instructions:[ComputeBudgetProgram.setComputeUnitLimit({units:450000}),ComputeBudgetProgram.setComputeUnitPrice({microLamports:priority}),create,...sharing?.instructions||[]]}).compileToV0Message(lookups); const transaction = new VersionedTransaction(message); if(transaction.serialize().length>1232)throw new Error('The launch details are too large. Shorten the coin name or metadata URL.'); await transaction.sign([mint],{lastValidBlockHeight:BigInt(recent.lastValidBlockHeight)}); onStep('simulate','running'); const simulation = await connection.simulateTransaction(transaction,{sigVerify:false,commitment:'confirmed'}); if(simulation.value.err) throw new Error(`Launch simulation failed (${JSON.stringify(simulation.value.err)}). No transaction was submitted.`); const fee = await connection.getFeeForMessage(message,'confirmed'); if(fee.value === null) throw new Error('Could not estimate network fee. Please retry.'); onEstimate(Number(fee.value) / 1e9); onStep('simulate','complete');onStep('sign','running'); const expectedMessage=message.serialize().slice(); const walletResult = await provider.signTransaction(transaction); const signed = VersionedTransaction.deserialize(walletResult.serialize()); if (!signed.message.serialize().every((b,i)=>b===expectedMessage[i]) || signed.message.serialize().length!==expectedMessage.length) throw new Error('Wallet returned a changed transaction.'); if(!await payer.verifySignature(signed.signatures[0],expectedMessage))throw new Error('The wallet did not provide a valid deployment signature.'); if(!await mint.publicKey.verifySignature(signed.signatures[1],expectedMessage))throw new Error('The wallet removed the mint signature. Please use a compatible wallet.'); const signature = base58Encode(signed.signatures[0]); const receipt={signature,mint:mint.publicKey.toBase58(),cluster,name,symbol,lastValidBlockHeight:String(recent.lastValidBlockHeight),state:'submitted',...(sharing?{feeSharing:{config:sharing.config,creatorBps:8000,platformBps:2000,platformWallet:PLATFORM_FEE_WALLET}}:{})}; // Retain the expected signature before submission, including when an RPC times out. onSubmitted(receipt);onStep('sign','complete');onStep('confirm','running'); try { const returned=await connection.sendRawTransaction(signed.serialize(),{skipPreflight:false,maxRetries:3,preflightCommitment:'confirmed'}); if(returned!==signature) throw new Error('RPC returned an unexpected transaction signature.'); await confirmLaunch(connection,receipt); } catch(error) { error.receipt??={...receipt,state:'unknown'}; throw error; } onStep('confirm','complete');return {...receipt,state:'confirmed'}; } export async function checkDeploymentReceipt(receipt,rpc){ const connection=new Connection(endpointFor(receipt.cluster,rpc),connectionOptions); if(await connection.getGenesisHash()!==genesis[receipt.cluster])throw new Error('RPC network does not match this receipt.'); const result=await connection.getSignatureStatuses([receipt.signature],{searchTransactionHistory:true});const status=result.value[0]; if(status?.err)return {...receipt,state:'failed'}; if(['confirmed','finalized'].includes(status?.confirmationStatus))return {...receipt,state:'confirmed'}; if(!status&&receipt.lastValidBlockHeight&&BigInt(await connection.getBlockHeight('confirmed'))>BigInt(receipt.lastValidBlockHeight))return {...receipt,state:'expired'}; return {...receipt,state:'unknown'}; }