// Solana Program Library (SPL) token primitives — PDA derivation, // Associated Token Account math, and the two Token-program instructions // this wallet needs at the bytecode level: `TransferChecked` (send SPL // with a decimals sanity check) and `CreateAssociatedTokenAccountIdempotent` // (make the receiver's token account inline, so the user doesn't have to // pre-create it on any address they've never seen before). // // Every account address on Solana is a 32-byte ed25519 public key. A // Program-Derived Address (PDA) is a 32-byte value that is NOT on the // ed25519 curve — the runtime uses that fact as proof that no one holds // its private key, so only the owning program can spend from it. To // derive a PDA we sha256(seeds || programId || bump || "ProgramDerivedAddress") // for bump = 255…0 and pick the first value that isn't a valid curve // point. `isOnCurve` here defers to @noble/curves/ed25519's ExtendedPoint, // which throws on invalid points; everything that decodes is on-curve. const PDA_MARKER = new TextEncoder().encode("ProgramDerivedAddress"); module.exports = function makeSolSpl({ ed25519, sha256, base58 }) { if (!ed25519 || !sha256 || !base58) throw new Error("sol-spl: missing dep"); // ---- constants ------------------------------------------------------- const TOKEN_PROGRAM_ID_B58 = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"; const ASSOC_PROGRAM_ID_B58 = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"; const TOKEN_2022_PROGRAM_ID_B58 = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"; const TOKEN_PROGRAM_ID = base58.decode(TOKEN_PROGRAM_ID_B58); const ASSOC_PROGRAM_ID = base58.decode(ASSOC_PROGRAM_ID_B58); const TOKEN_2022_PROGRAM_ID = base58.decode(TOKEN_2022_PROGRAM_ID_B58); const SYSTEM_PROGRAM_ID = new Uint8Array(32); // Known-token registry — just enough to give the panel a sensible label // for the tokens users actually see day-to-day. Everything else falls // back to the mint address itself (truncated in the UI). const KNOWN_TOKENS = { mainnet: { "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": { symbol: "USDC", decimals: 6, name: "USD Coin" }, "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB": { symbol: "USDT", decimals: 6, name: "Tether USD" }, "So11111111111111111111111111111111111111112": { symbol: "wSOL", decimals: 9, name: "Wrapped SOL" }, }, devnet: { "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU": { symbol: "USDC", decimals: 6, name: "USD Coin (devnet)" }, }, }; // ---- ed25519 curve check -------------------------------------------- // A 32-byte pubkey is "on curve" if it decompresses to a valid Edwards // point. @noble/curves v2 exposes Point.fromBytes(bytes) (throws on // invalid); older versions exposed ExtendedPoint.fromHex(hex-string) // — try each in order. function isOnCurve(pubkey32) { const P = ed25519.Point || ed25519.ExtendedPoint; if (!P) return true; // no curve access — treat every value as // on-curve (over-conservative; PDA loop falls // through more than it should but never gets // wrong). try { if (typeof P.fromBytes === "function") { P.fromBytes(pubkey32); return true; } if (typeof P.fromHex === "function") { const hex = Array.from(pubkey32, (b) => b.toString(16).padStart(2, "0")).join(""); P.fromHex(hex); return true; } } catch { return false; } return true; } const concat = (...ps) => { const n = ps.reduce((a, p) => a + p.length, 0); const out = new Uint8Array(n); let k = 0; for (const p of ps) { out.set(p, k); k += p.length; } return out; }; const u64le = (n) => { let v = BigInt(n); const o = new Uint8Array(8); for (let i = 0; i < 8; i++) { o[i] = Number(v & 0xffn); v >>= 8n; } return o; }; const encodeCompactU16 = (n) => { const out = []; let rem = n; while (true) { let byte = rem & 0x7f; rem >>= 7; if (rem === 0) { out.push(byte); break; } byte |= 0x80; out.push(byte); } return Uint8Array.from(out); }; // ---- PDA / ATA ------------------------------------------------------- function findProgramAddress(seeds, programId) { for (let bump = 255; bump >= 0; bump--) { const material = concat( ...seeds.map((s) => Uint8Array.from(s)), Uint8Array.from([bump]), programId, PDA_MARKER, ); const candidate = sha256(material); if (!isOnCurve(candidate)) return { address: candidate, bump }; } throw new Error("no PDA found (unreachable)"); } // Standard ATA = PDA under ASSOC_PROGRAM_ID with seeds // [ownerPubkey, TOKEN_PROGRAM_ID, mint]. // We match the seed layout the @solana/spl-token library uses so // addresses agree with any wallet or block explorer. function associatedTokenAddress(owner, mint, tokenProgram = TOKEN_PROGRAM_ID) { return findProgramAddress([owner, tokenProgram, mint], ASSOC_PROGRAM_ID).address; } // ---- instruction encoders ------------------------------------------- // SPL Token: TransferChecked (discriminator 12) — asserts amount+decimals // against the mint so a UI bug can't move 1000× the intended value. // accounts: [sourceATA (writable), mint (readonly), destATA (writable), owner (signer)] // data: [12, amount:u64_le, decimals:u8] function transferCheckedInstruction({ sourceATA, mint, destATA, owner, amount, decimals, tokenProgram = TOKEN_PROGRAM_ID }) { return { programId: tokenProgram, keys: [ { pubkey: sourceATA, isSigner: false, isWritable: true }, { pubkey: mint, isSigner: false, isWritable: false }, { pubkey: destATA, isSigner: false, isWritable: true }, { pubkey: owner, isSigner: true, isWritable: false }, ], data: concat(Uint8Array.from([12]), u64le(amount), Uint8Array.from([decimals])), }; } // Associated Token Account program: CreateIdempotent (discriminator 1) // accounts: [payer(signer,writable), ata(writable), owner(readonly), // mint(readonly), systemProgram(readonly), tokenProgram(readonly)] // data: [1] (idempotent variant — no-op if the account exists) function createATAIdempotentInstruction({ payer, ata, owner, mint, tokenProgram = TOKEN_PROGRAM_ID }) { return { programId: ASSOC_PROGRAM_ID, 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: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false }, { pubkey: tokenProgram, isSigner: false, isWritable: false }, ], data: Uint8Array.from([1]), }; } // ---- transaction message builder ------------------------------------ // Assembles the raw Solana message bytes for a single-fee-payer, single- // signer transaction that may carry multiple instructions. Account keys // are sorted per Solana's account-classification rules (writable-signed, // readonly-signed, writable-unsigned, readonly-unsigned). function buildMessage({ feePayer, instructions, recentBlockhash }) { // 1. Collect every unique pubkey mentioned across instructions + // include the fee payer + include each instruction's programId. const keys = new Map(); // b58 → { pubkey, isSigner, isWritable } const upsert = (pubkey, isSigner, isWritable) => { const k = base58.encode(pubkey); const cur = keys.get(k) || { pubkey, isSigner: false, isWritable: false }; cur.isSigner = cur.isSigner || isSigner; cur.isWritable = cur.isWritable || isWritable; keys.set(k, cur); }; upsert(feePayer, true, true); for (const ins of instructions) { for (const k of ins.keys) upsert(k.pubkey, k.isSigner, k.isWritable); upsert(ins.programId, false, false); } // 2. Classify + sort into the four buckets. const bucket = { ws: [], rs: [], wu: [], ru: [] }; for (const v of keys.values()) { if (v.isSigner && v.isWritable) bucket.ws.push(v); else if (v.isSigner) bucket.rs.push(v); else if (v.isWritable) bucket.wu.push(v); else bucket.ru.push(v); } // Fee payer MUST be at index 0 (Solana requires the first signer to // be writable & to pay the fee). const payerB58 = base58.encode(feePayer); bucket.ws.sort((a, b) => (base58.encode(a.pubkey) === payerB58 ? -1 : base58.encode(b.pubkey) === payerB58 ? 1 : 0)); const ordered = [...bucket.ws, ...bucket.rs, ...bucket.wu, ...bucket.ru]; // 3. Encode the message. const header = Uint8Array.from([ bucket.ws.length + bucket.rs.length, // numRequiredSignatures bucket.rs.length, // numReadonlySignedAccounts bucket.ru.length, // numReadonlyUnsignedAccounts ]); const keysSection = concat( encodeCompactU16(ordered.length), ...ordered.map((v) => Uint8Array.from(v.pubkey)), ); const indexOf = new Map(ordered.map((v, i) => [base58.encode(v.pubkey), i])); const insSection = concat( encodeCompactU16(instructions.length), ...instructions.map((ins) => { const programIndex = indexOf.get(base58.encode(ins.programId)); const accountBytes = Uint8Array.from(ins.keys.map((k) => indexOf.get(base58.encode(k.pubkey)))); return concat( Uint8Array.from([programIndex]), encodeCompactU16(accountBytes.length), accountBytes, encodeCompactU16(ins.data.length), ins.data, ); }), ); return concat(header, keysSection, recentBlockhash, insSection); } return { // constants TOKEN_PROGRAM_ID, ASSOC_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_PROGRAM_ID_B58, ASSOC_PROGRAM_ID_B58, KNOWN_TOKENS, // helpers isOnCurve, findProgramAddress, associatedTokenAddress, // instruction encoders transferCheckedInstruction, createATAIdempotentInstruction, // tx assembly buildMessage, }; };