SPB Git

Repository Public

build-sol.ts 5.8 KB 103 lines typescript

1import { sha256 } from '@noble/hashes/sha2.js';2import { ed25519 } from '@noble/curves/ed25519.js';3import { base58 } from '@scure/base';4import { baseUnits } from './amounts.ts';5import { normalizeAddress } from './address.ts';6import { getAsset } from './networks.ts';7import type { Intent } from './intent.ts';89export const SYSTEM_PROGRAM = '11111111111111111111111111111111';10export const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';11export const TOKEN_2022_PROGRAM = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';12export const ATA_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';13export const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';14const enc = new TextEncoder();1516function isOnCurve(b: Uint8Array): boolean {17  try { ed25519.Point.fromBytes(b); return true; } catch { return false; }18}1920export function findProgramAddress(seeds: Uint8Array[], programId: string): string {21  const pid = base58.decode(programId);22  for (let bump = 255; bump >= 0; bump--) {23    const h = sha256.create();24    for (const s of seeds) h.update(s);25    h.update(new Uint8Array([bump])).update(pid).update(enc.encode('ProgramDerivedAddress'));26    const out = h.digest();27    if (!isOnCurve(out)) return base58.encode(out);28  }29  throw new Error('no PDA found');30}3132export function associatedTokenAddress(owner: string, mint: string, tokenProgram: string = TOKEN_PROGRAM): string {33  return findProgramAddress([base58.decode(owner), base58.decode(tokenProgram), base58.decode(mint)], ATA_PROGRAM);34}35export const tokenProgramOf = (a: { program?: 'token' | 'token2022' }) => (a.program === 'token2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);3637function compactU16(n: number): number[] {38  const out: number[] = [];39  for (;;) { const b = n & 0x7f; n >>= 7; if (n === 0) { out.push(b); return out; } out.push(b | 0x80); }40}41const u32le = (n: number) => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; };42const u64le = (n: bigint) => { if (n < 0n || n >= 1n << 64n) throw new Error('u64 overflow'); const b = new Uint8Array(8); new DataView(b.buffer).setBigUint64(0, n, true); return b; };43const cat = (...a: (Uint8Array | number[])[]) => Uint8Array.from(a.flatMap((x) => Array.from(x)));4445interface Ix { program: string; accounts: string[]; data: Uint8Array }4647/** Rebuild the exact legacy transaction message bytes from an intent. Fixed instruction set only. */48export function buildSolMessage(i: Intent): Uint8Array {49  if (i.network !== 'solana' || !i.sol) throw new Error('not a Solana intent');50  const asset = getAsset('solana', i.asset);51  if (asset.decimals !== i.decimals || asset.symbol !== i.symbol) throw new Error('asset metadata mismatch');52  const from = normalizeAddress('solana', i.from), to = normalizeAddress('solana', i.to);53  if (from === to) throw new Error('source equals destination');54  const tp = tokenProgramOf(asset);55  const reserved = [SYSTEM_PROGRAM, TOKEN_PROGRAM, TOKEN_2022_PROGRAM, ATA_PROGRAM, COMPUTE_BUDGET_PROGRAM, asset.id];56  if (reserved.includes(to)) throw new Error('destination is a program or mint');57  const amount = baseUnits(i.amount);58  if (amount <= 0n) throw new Error('amount must be positive');59  const price = baseUnits(i.sol.computeUnitPrice);60  // fee = 5000 lamports/signature + ceil(limit * microLamports / 1e6)61  const fee = 5000n + (BigInt(i.sol.computeUnitLimit) * price + 999_999n) / 1_000_000n;62  if (fee > baseUnits(i.maxFee)) throw new Error('fee exceeds approved maximum');63  const blockhash = base58.decode(i.sol.recentBlockhash);64  if (blockhash.length !== 32) throw new Error('bad blockhash');6566  const ixs: Ix[] = [67    { program: COMPUTE_BUDGET_PROGRAM, accounts: [], data: cat([2], u32le(i.sol.computeUnitLimit)) },68    { program: COMPUTE_BUDGET_PROGRAM, accounts: [], data: cat([3], u64le(price)) },69  ];70  let writable: string[], readonly: string[];71  if (asset.id === 'native') {72    if (i.sol.createAta) throw new Error('createAta is invalid for native transfers');73    ixs.push({ program: SYSTEM_PROGRAM, accounts: [from, to], data: cat(u32le(2), u64le(amount)) });74    writable = [to]; readonly = [SYSTEM_PROGRAM, COMPUTE_BUDGET_PROGRAM];75  } else {76    const src = associatedTokenAddress(from, asset.id, tp), dst = associatedTokenAddress(to, asset.id, tp);77    if (i.sol.createAta) ixs.push({ program: ATA_PROGRAM, accounts: [from, dst, to, asset.id, SYSTEM_PROGRAM, tp], data: Uint8Array.of(1) });78    ixs.push({ program: tp, accounts: [src, asset.id, dst, from], data: cat([12], u64le(amount), [i.decimals]) }); // TransferChecked (same layout in Token-2022)79    writable = [src, dst];80    readonly = i.sol.createAta81      ? [to, asset.id, SYSTEM_PROGRAM, tp, ATA_PROGRAM, COMPUTE_BUDGET_PROGRAM]82      : [asset.id, tp, COMPUTE_BUDGET_PROGRAM];83  }84  const keys = [from, ...writable, ...readonly];85  if (new Set(keys).size !== keys.length) throw new Error('duplicate account keys');86  const idx = (k: string) => { const n = keys.indexOf(k); if (n < 0) throw new Error('unknown key'); return n; };87  const body: (Uint8Array | number[])[] = [[1, 0, readonly.length], compactU16(keys.length)];88  for (const k of keys) body.push(base58.decode(k));89  body.push(blockhash, compactU16(ixs.length));90  for (const ix of ixs) body.push([idx(ix.program)], compactU16(ix.accounts.length), ix.accounts.map(idx), compactU16(ix.data.length), ix.data);91  return cat(...body);92}9394/** Wire format: compact signature count + 64-byte signature + message. */95export function solWireTransaction(signature: Uint8Array, message: Uint8Array): Uint8Array {96  if (signature.length !== 64) throw new Error('bad signature length');97  return cat([1], signature, message);98}99100export function solFeeLamports(computeUnitLimit: number, computeUnitPrice: bigint): bigint {101  return 5000n + (BigInt(computeUnitLimit) * computeUnitPrice + 999_999n) / 1_000_000n;102}103
Files main
Menu

Theme