apps/web/lib/solana/verify.ts
The server checks the record against the chain. Shown whole, as it was in the repository when this site was built. Line numbers link: add #L12 to the address.
apps/web/lib/solana/verify.ts88 lines
1import 'server-only';2// Server checks for a Solana launch record: the ed25519 signature (node:crypto) and the on-chain state that makes the3// coin a Solary coin: the launch transaction was paid by the deployer and created that mint; the curve's creator is4// the mint's pump.fun sharing config; its shareholders are exactly sharingShareholders(feeSplit(holdersBps), vault,5// deployer), locked; the Token-2022 metadata carries the signed name, ticker and metadata URI.6import { createPublicKey, verify as edVerify } from 'node:crypto';7import { PublicKey } from '@solana/web3.js';8import { feeSplit, sharingShareholders } from '@solary/core/fees';9import { base58Decode, decodeBondingCurve, decodeSharingConfig, decodeToken2022Metadata, sharingConfigLocked } from '@solary/core/pump';10import { upstreamRpc } from '@/lib/server-env';11import { pumpPda } from './pump';12 13export function verifyEd25519(message: Uint8Array, signatureB58: string, publicKeyB58: string): boolean {14 try {15 const sig = base58Decode(signatureB58);16 const pub = base58Decode(publicKeyB58);17 if (sig.length !== 64 || pub.length !== 32) return false;18 const key = createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: Buffer.from(pub).toString('base64url') }, format: 'jwk' });19 return edVerify(null, message, key, sig);20 } catch {21 return false;22 }23}24 25async function rpc<T>(method: string, params: unknown[]): Promise<T> {26 const r = await fetch(upstreamRpc(), {27 method: 'POST',28 headers: { 'content-type': 'application/json' },29 body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),30 signal: AbortSignal.timeout(15_000),31 cache: 'no-store',32 });33 const j = (await r.json()) as { result?: T; error?: { message: string } };34 if (j.error) throw new Error(`${method}: ${j.error.message}`);35 return j.result as T;36}37 38export type LaunchCheck = { ok: true } | { ok: false; status: number; error: string };39 40const sameShareholders = (a: ReadonlyArray<{ address: string; bps: number }>, b: ReadonlyArray<{ address: string; bps: number }>) =>41 a.length === b.length && a.every((x, i) => x.address === b[i]!.address && x.bps === b[i]!.bps);42 43/** The chain agrees with the record: see the file comment. `409` means "not visible yet, retry". */44export async function checkSolanaLaunch(a: {45 mint: string;46 tx: string;47 deployer: string;48 vault: string;49 holdersBps: number;50 name: string;51 symbol: string;52 metadataUri?: string;53}): Promise<LaunchCheck> {54 type TxRes = { meta: { err: unknown } | null; transaction: { message: { accountKeys: string[] } } } | null;55 const t = await rpc<TxRes>('getTransaction', [a.tx, { encoding: 'json', commitment: 'confirmed', maxSupportedTransactionVersion: 0 }]);56 if (!t) return { ok: false, status: 409, error: 'launch transaction not found yet; retry in a few seconds' };57 if (!t.meta || t.meta.err) return { ok: false, status: 400, error: 'launch transaction failed on-chain' };58 const keys = t.transaction.message.accountKeys;59 if (keys[0] !== a.deployer) return { ok: false, status: 400, error: 'launch transaction was paid by a different wallet' };60 if (!keys.includes(a.mint)) return { ok: false, status: 400, error: 'launch transaction does not touch this mint' };61 62 const mint = new PublicKey(a.mint);63 const cfg = pumpPda.sharingConfig(mint);64 const res = await rpc<{ value: Array<{ data: [string, string] } | null> }>('getMultipleAccounts', [65 [pumpPda.bondingCurve(mint).toBase58(), cfg.toBase58(), a.mint],66 { encoding: 'base64', commitment: 'confirmed' },67 ]);68 const [bcA, cfgA, mintA] = res.value;69 if (!bcA || !mintA) return { ok: false, status: 409, error: 'coin not visible yet; retry in a few seconds' };70 const bytes = (x: { data: [string, string] }) => new Uint8Array(Buffer.from(x.data[0], 'base64'));71 const bc = decodeBondingCurve(bytes(bcA));72 // a two-transaction launch sets the split in its second transaction73 if (!cfgA || bc.creator !== cfg.toBase58()) return { ok: false, status: 409, error: "the coin's fee split is not set yet; finish the launch" };74 const sc = decodeSharingConfig(bytes(cfgA));75 const expected = sharingShareholders(feeSplit(a.holdersBps), a.vault, a.deployer);76 if (!sameShareholders(sc.shareholders, expected)) {77 // editable and still pointing at the deployer: the split transaction has not landed yet78 if (!sharingConfigLocked(sc)) return { ok: false, status: 409, error: "the coin's fee split is not set yet; finish the launch" };79 return { ok: false, status: 400, error: "the coin's fee split does not match the signed record" };80 }81 if (!sharingConfigLocked(sc)) return { ok: false, status: 400, error: "the coin's fee split is not locked" };82 const md = decodeToken2022Metadata(bytes(mintA));83 if (md) {84 if (md.name.trim() !== a.name || md.symbol.trim().toUpperCase() !== a.symbol) return { ok: false, status: 400, error: 'name or ticker differ from the coin on-chain' };85 if (a.metadataUri && md.uri.trim() !== a.metadataUri) return { ok: false, status: 400, error: 'metadata address differs from the coin on-chain' };86 }87 return { ok: true };88}