Setup 5d15508bba929f1f074c052ac933863eadf6eb8e56984ebd5a1af75e80626643 Portable a5d346b97f5a13d85fa3bd301a72075ddb82fe636d7b1a51840ffd5a16d879f4 Bundled since 0.3.27: 32d4b75 - Aegis (bchwallet) gains its own update card in Settings > General beside Ariadne. Check for updates hits the same signed OTA endpoint the boot timer uses; Restart to apply appears when a signed newer version is staged. Uses the existing addons-check-updates + a new app-restart IPC. New Aegis versions ship without a Theseus release. 32d4b75 (same commit) - DevTools (F12 / Ctrl+Shift+I) opens docked to the right of the tab (mode: 'right') instead of a detached window. Matches stock Chrome. Users who prefer detached can drag out via the DevTools own toolbar. b71c925 - Search-engine favicons in Settings > Search now use Google's /s2/favicons service — DuckDuckGo's ip3 source returned 404 for enough hosts (Brave, Bing, Yandex, etc.) that half the list was falling through to the emoji placeholder. Deployed. Verified LIVE 0.3.28.
220 lines
10 KiB
JavaScript
220 lines
10 KiB
JavaScript
// 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,
|
||
};
|
||
};
|