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}