UNPKG

micro-sol-signer

Version:

Create, sign & decode Solana transactions with minimum deps

664 lines (657 loc) 25.6 kB
import { ed25519 } from '@noble/curves/ed25519.js'; import { sha256 } from '@noble/hashes/sha2.js'; import { concatBytes } from '@noble/hashes/utils.js'; import { base16, base58, base64, utf8 } from '@scure/base'; import * as P from 'micro-packed'; /* # What is IDL? Solana IDL == Ethereum ABI. Docs: https://github.com/codama-idl/codama/tree/main/packages/nodes # IDLS - Token: https://github.com/solana-program/token/blob/main/program/idl.json - Token2022: https://github.com/solana-program/token-2022/blob/main/program/idl.json - System: https://raw.githubusercontent.com/solana-program/system/refs/heads/main/program/idl.json - ALT: https://github.com/solana-program/address-lookup-table/blob/main/program/idl.json - Stake: https://raw.githubusercontent.com/solana-program/stake/refs/heads/main/program/idl.json - Memo: https://raw.githubusercontent.com/solana-program/memo/refs/heads/main/program/idl.json - Compute budget: https://raw.githubusercontent.com/solana-program/compute-budget/refs/heads/main/program/idl.json - Config: https://raw.githubusercontent.com/solana-program/config/refs/heads/main/program/idl.json These are anchor v00/v01, but it is possible to convert these to codama: - Raydium CL: https://solscan.io/account/CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK#anchorProgramIdl - Jupyter: https://solscan.io/account/JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4#anchorProgramIdl ## Status: - this is slightly less broken than previous version (id) - a lot of bugs fixed that was in previous version - super fragile and likely broken (you may lose funds!) ## Not done - multisig support - types - PDA as parseValue - IDL mostly works, but I don't trust it - various link/semantic node values - not padded preOffsetTypeNode/postOffsetTypeNode: unclear how to do this without adjusting micro-packed - does not seem to be used */ // Utils export const PRECISION = 9; export const Decimal = P.coders.decimal(PRECISION); const b58 = () => { const inner = P.bytes(32); return P.wrap({ size: inner.size, encodeStream: (w, value) => inner.encodeStream(w, base58.decode(value)), decodeStream: (r) => base58.encode(inner.decodeStream(r)), }); }; // first bit -- terminator (1 -- continue, 0 -- last) export const shortU16 = P.wrap({ encodeStream: (w, value) => { if (!value) return w.byte(0); for (; value; value >>= 7) { w.bits(value > 0x7f ? 1 : 0, 1); w.bits(value & 0x7f, 7); } }, decodeStream: (r) => { let len = 0; for (let pos = 0; !r.isEnd(); pos++) { const last = !r.bits(1); len |= r.bits(7) << (pos * 7); if (last) break; } return len; }, }); export const pubKey = b58(); function mod(a, b = ed25519.Point.Fp.ORDER) { const res = a % b; return res >= 0n ? res : b + res; } export function isOnCurve(bytes) { if (typeof bytes === 'string') bytes = base58.decode(bytes); try { // noble-ed25519 checks that publicKey is < P, but dalek (ed25519-dalek.CompressedEdwardsY) is not, so we do modulo here. // first bit in last byte is x oddity flag const last = bytes[31]; const normedLast = last & ~0x80; const normed = Uint8Array.from(Array.from(bytes.slice(0, 31)).concat(normedLast)); const modBytes = P.U256LE.encode(mod(P.U256LE.decode(normed))); if ((last & 0x80) !== 0) modBytes[31] |= 0x80; ed25519.Point.fromBytes(modBytes); return true; } catch (e) { return false; } } export function programAddress(program, ...seeds) { let seed = P.utils.concatBytes(...seeds); const noncePos = seed.length; seed = P.utils.concatBytes(seed, Uint8Array.of(0), base58.decode(program), utf8.decode('ProgramDerivedAddress')); for (let i = 255; i >= 0; i--) { seed[noncePos] = i; const hash = sha256(seed); if (isOnCurve(hash)) continue; return base58.encode(hash); } throw new Error('SOL.programAddress: nonce exhausted, cannot find program address'); } // Boolean based on arbitrary number const numBool = { encode: (from) => { if (from === 1) return true; if (from === 0) return false; throw new Error('wrong boolean'); }, decode(to) { if (to === true) return 1; if (to === false) return 0; throw new Error('wrong boolean'); }, }; // Add postfix to string const stringPostfix = (postfix) => ({ encode(from) { return from + postfix; }, decode(to) { if (!to.endsWith(postfix)) throw new Error('wrong postfix'); return to.slice(0, -postfix.length); }, }); // Opposite of P.coders.numberBigint: use bigints with u8/u16/u32 const fromBigint = { encode: (from) => { if (!Number.isSafeInteger(from)) throw new Error(`expected safe number, got ${typeof from}`); return BigInt(from); }, decode: (to) => { if (typeof to !== 'bigint') throw new Error(`expected bigint, got ${typeof to}`); if (to > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`element bigger than MAX_SAFE_INTEGER=${to}`); return Number(to); }, }; const defaultCoder = (inner, value) => P.apply(inner, { encode: (from) => from, decode: (to) => (to === undefined ? value : to), }); // TODO: it should be done via flags? function zeroable(inner) { if (!Number.isSafeInteger(inner.size)) throw new Error('zeroable on unsized element'); const ZEROS = new Uint8Array(inner.size); return P.wrap({ size: inner.size, encodeStream(w, value) { if (value === undefined) w.bytes(ZEROS); else inner.encodeStream(w, value); }, decodeStream: inner.decodeStream, }); } function remainder(inner) { return P.wrap({ size: inner.size, encodeStream(w, value) { if (value !== undefined) inner.encodeStream(w, value); }, decodeStream(r) { if (r.isEnd()) return undefined; return inner.decodeStream(r); }, }); } function prefix(inner, prefix) { return P.wrap({ size: inner.size, encodeStream(w, value) { w.bytes(prefix); inner.encodeStream(w, value); }, decodeStream(r) { const p = r.bytes(prefix.length); if (!P.utils.equalBytes(p, prefix)) throw new Error('wrong prefix'); return inner.decodeStream(r); }, }); } function postfix(inner, postfix) { return P.wrap({ size: inner.size, encodeStream(w, value) { inner.encodeStream(w, value); w.bytes(postfix); }, decodeStream(r) { const res = inner.decodeStream(r); if (!P.utils.equalBytes(r.bytes(postfix.length), postfix)) throw new Error('wrong postfix'); return res; }, }); } const EMPTY = P.magic(P.bytes(0), new Uint8Array(0)); function fixedOptional(flag, inner) { if (!P.isCoder(flag) || !P.isCoder(inner)) throw new Error(`fixedOptional: invalid flag or inner value flag=${flag} inner=${inner}`); if (flag.size === undefined) throw new Error('fixedOptional with unsized flag'); if (inner.size === undefined) throw new Error('fixedOptional with unsized inner'); return P.wrap({ size: flag.size + inner.size, encodeStream: (w, value) => { flag.encodeStream(w, !!value); if (value) inner.encodeStream(w, value); else w.bytes(new Uint8Array(inner.size)); }, decodeStream: (r) => { if (flag.decodeStream(r)) return inner.decodeStream(r); else { if (!P.utils.equalBytes(r.bytes(inner.size), new Uint8Array(inner.size))) throw new Error('fixedOptional: wrong padding'); } return; }, }); } function parseValueInt(value, _pdas, _dt) { // Everything is bigint, except things that used as counters (array length/etc) if (value.kind === 'numberValueNode') return value.number; if (value.kind === 'noneValueNode') return undefined; if (value.kind === 'booleanValueNode') return value.boolean; if (value.kind === 'bytesValueNode') { if (value.encoding === 'base16') return base16.decode(value.data.toUpperCase()); if (value.encoding === 'base58') return base58.decode(value.data); if (value.encoding === 'base64') return base64.decode(value.data); if (value.encoding === 'utf8') return utf8.decode(value.data); } if (value.kind === 'publicKeyValueNode') return value.publicKey; if (value.kind === 'pdaValueNode') { throw new Error('not implemented'); // if (value.pda.kind !== 'pdaLinkNode') throw new Error('wrong pda link node'); // const link = pdas[value.pda.name]; // if (!link) throw new Error('unknown pda link:' + value.pda.name); // // TODO: fix? // const seeds = Object.fromEntries( // value.seeds.map((i) => { // if (i.kind !== 'pdaSeedValueNode') throw new Error('unknown pda seed node'); // if (!['accountValueNode', 'argumentValueNode'].includes(i.value.kind)) // throw new Error('wrong pda seed node'); // //console.log('T', i.value.name); // return [i.name, i]; // }) // ); } throw new Error('wrong default value'); } const IGNORE_DEFAULT = [ 'payerValueNode', 'accountBumpValueNode', 'identityValueNode', 'pdaValueNode', ]; function parseValue(node, val, pdas, dt) { if (node.defaultValue) { // These not availabe on parsing step if (IGNORE_DEFAULT.includes(node.defaultValue.kind)) { return val; } if (val !== undefined && node.defaultValueStrategy === 'omitted') throw new Error('parseValue: non-empty omitted value'); if (val === undefined) return parseValueInt(node.defaultValue, pdas, dt); } return val; } // Types // prettier-ignore const NumCoders = { shortU16: { le: shortU16, be: shortU16, bigint: false }, // Solana u8: { le: P.U8, be: P.U8, bigint: false }, // Unsigned u16: { le: P.U16LE, be: P.U16BE, bigint: false }, u32: { le: P.U32LE, be: P.U32BE, bigint: false }, u64: { le: P.U64LE, be: P.U64BE, bigint: true }, u128: { le: P.U128LE, be: P.U128BE, bigint: true }, i8: { le: P.I8, be: P.I8, bigint: false }, // Signed i16: { le: P.I16LE, be: P.I16BE, bigint: false }, i32: { le: P.I32LE, be: P.I32BE, bigint: false }, i64: { le: P.I64LE, be: P.I64BE, bigint: true }, i128: { le: P.I128LE, be: P.I128BE, bigint: true }, f32: { le: P.F32LE, be: P.F32BE, bigint: false }, // Float f64: { le: P.F64LE, be: P.F64BE, bigint: false }, }; // As bigint function parseNumeric(type) { if (type.kind !== 'numberTypeNode') throw new Error('wrong numberTypeNode'); const endian = type.endian || 'le'; if (endian !== 'le' && endian !== 'be') throw new Error('numberTypeNode: wrong endian'); let format = NumCoders[type.format][endian]; if (!format) throw new Error('wrong numeric type'); // Allow writing number to bigint coders const isBigint = NumCoders[type.format].bigint; if (isBigint) { return P.apply(format, { encode: (from) => from, decode(to) { if (typeof to !== 'bigint' && Number.isSafeInteger(to)) return BigInt(to); return to; }, }); } return format; } // As number (for counts). TODO: merge with parseNumeric function parseNumericSafe(type) { const t = parseNumeric(type); const isBigint = NumCoders[type.format].bigint; // On read replace bigints with numbers if (isBigint) { return P.apply(t, { encode(from) { if (from > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`element bigger than MAX_SAFE_INTEGER=${from}`); return Number(from); }, decode: (to) => to, }); } return t; } function parseCount(count) { if (count.kind === 'prefixedCountNode') return parseNumericSafe(count.prefix); if (count.kind === 'remainderCountNode') return null; if (count.kind === 'fixedCountNode') { if (!Number.isSafeInteger(count.value)) throw new Error('wrong fixedCountNode'); return count.value; } throw new Error('wrong count node'); } const types = { // Primitive publicKeyTypeNode: () => pubKey, numberTypeNode: (type) => parseNumeric(type), booleanTypeNode: (type) => P.apply(parseNumericSafe(type.size), numBool), bytesTypeNode: (_type) => P.bytes(null), // Strip zero bytes from string: ugly, but required for compatibility with solana utf8 coder stringTypeNode: (_type) => P.validate(P.string(null), (s) => s.replace(/\u0000/g, '')), amountTypeNode: (type) => { let x = parseNumeric(type.number); if (!NumCoders[type.number.format].bigint) x = P.apply(x, fromBigint); // fromBigint const x2 = P.apply(x, P.coders.decimal(type.decimals)); return P.apply(x2, stringPostfix(` ${type.unit}`)); }, // Wrappers fixedSizeTypeNode: (type, dt = {}) => P.prefix(type.size, mapType(type.type, dt)), sizePrefixTypeNode: (type, dt = {}) => P.prefix(parseNumericSafe(type.prefix), mapType(type.type, dt)), optionTypeNode: (type, dt = {}) => { const inner = mapType(type.item, dt); const prefix = parseNumericSafe(type.prefix ? type.prefix : { kind: 'numberTypeNode', format: 'u8', endian: 'le' }); if (type.fixed === true) { if (!inner.size) throw new Error('optional fixed=true with unsized element'); return fixedOptional(P.apply(prefix, numBool), inner); } return P.optional(P.apply(prefix, numBool), inner); }, // Structure arrayTypeNode: (type, dt = {}) => P.array(parseCount(type.count), mapType(type.item, dt)), enumVariant: (type, dt = {}) => { if (type.kind === 'enumStructVariantTypeNode') return mapType(type.struct, dt); if (type.kind === 'enumTupleVariantTypeNode') return mapType(type.tuple, dt); if (type.kind === 'enumEmptyVariantTypeNode') return EMPTY; throw new Error('unknown enum variant'); }, enumTypeNode: (type, dt = {}) => { const variants = Object.fromEntries(type.variants.map((i, j) => [i.name, [i.discriminator || j, types.enumVariant(i, dt)]])); return P.mappedTag(parseNumericSafe(type.size), variants); }, mapTypeNode: (type, dt = {}) => { const inner = P.tuple([mapType(type.key, dt), mapType(type.value, dt)]); const lst = P.array(parseCount(type.count), inner); return P.apply(lst, P.coders.dict()); }, structFieldTypeNode: (type, dt = {}) => mapType({ ...type.type, defaultValue: type.defaultValue, defaultValueStrategy: type.defaultValueStrategy, }, dt), structTypeNode: (type, dt) => P.struct(Object.fromEntries(type.fields.map((i) => { if (i.kind !== 'structFieldTypeNode') throw new Error('wrong structFieldTypeNode'); return [i.name, mapType(i, dt)]; }))), tupleTypeNode: (type, dt = {}) => P.tuple(type.items.map((i) => mapType(i, dt))), definedTypeLinkNode: (type, dt = {}) => { return P.lazy(() => { if (!dt[type.name]) throw new Error('unknown type: ' + type.name); return dt[type.name]; }); }, zeroableOptionTypeNode: (type, dt = {}) => zeroable(mapType(type.item, dt)), remainderOptionTypeNode: (type, dt = {}) => remainder(mapType(type.item, dt)), constantValueNode: (type, dt = {}) => P.magic(mapType(type.type, dt), parseValueInt(type.value, {}, dt)), hiddenPrefixTypeNode: (type, dt = {}) => { return prefix(mapType(type.type, dt), concatBytes(...type.prefix.map((i) => mapType(i, dt).encode()))); }, hiddenSuffixTypeNode: (type, dt = {}) => postfix(mapType(type.type, dt), concatBytes(...type.suffix.map((i) => mapType(i, dt).encode()))), preOffsetTypeNode: (type, dt = {}) => { if (type.strategy === 'padded') return prefix(mapType(type.type, dt), new Uint8Array(type.offset)); // TODO: this includes very complex pointer-like manipulation that I'm not sure how to implement yet. throw new Error('not implemented'); }, postOffsetTypeNode: (type, dt = {}) => { if (type.strategy === 'padded') return postfix(mapType(type.type, dt), new Uint8Array(type.offset)); throw new Error('not implemented'); }, }; function mapTypeInternal(type, definedTypes = {}) { const t = types[type.kind]; if (t === undefined) throw new Error('Unknown type: ' + type.kind); return t(type, definedTypes); } export function mapType(type, dt) { const t = mapTypeInternal(type, dt); // Inner type of field type is already mapped! if (type.defaultValue && type.kind !== 'structFieldTypeNode' && !IGNORE_DEFAULT.includes(type.defaultValue.kind)) { const def = parseValueInt(type.defaultValue, {}, dt); if (type.defaultValueStrategy === 'omitted') return P.magic(t, def); if (type.defaultValueStrategy === 'optional' || type.defaultValueStrategy === undefined) return defaultCoder(t, def); throw new Error('wrong defaultValueStrategy: ' + type.defaultValueStrategy); } return t; } function parseDefinedTypes(types) { const res = {}; // Disable recursive stuff here for (const t of types) res[t.name] = mapType(t.type, res); return res; } export function parsePDAs(program, pda, dt = {}) { const res = {}; for (const p of pda) { const fields = Object.fromEntries(p.seeds.map((seed) => { if (seed.kind === 'variablePdaSeedNode') return [seed.name, mapType(seed.type, dt)]; if (seed.kind === 'constantPdaSeedNode') { // TODO: check return [ seed.name, P.magic(mapType(seed.type, dt), parseValueInt(seed.value, res, dt)), ]; } throw new Error('unknown seed type'); })); const coder = P.struct(fields); res[p.name] = (value) => programAddress(program, coder.encode(value)); } return res; } function parseArguments(args, types) { const res = {}; for (const a of args) { if (a.kind !== 'instructionArgumentNode') throw new Error('instructionArgumentNode'); const type = mapType({ ...a.type, defaultValue: a.defaultValue, defaultValueStrategy: a.defaultValueStrategy }, types); res[a.name] = type; } return res; } function getFieldBytes(node, field, types) { if (node.kind === 'accountNode') { if (node.data.kind === 'structTypeNode') { for (const f of node.data.fields) { if (f.name !== field) continue; return mapType(f, types).encode(undefined); } } } if (node.kind === 'instructionNode') { for (const f of node.arguments) { if (f.name !== field) continue; return mapType({ ...f.type, defaultValue: f.defaultValue, defaultValueStrategy: f.defaultValueStrategy }, types).encode(undefined); } } throw new Error('getFieldBytes wrong node type: ' + node.kind); } function decodeDiscriminators(discriminators, coder, node, types) { return (data, opts) => { // This is slower and worse than previous version via tag, but significantly more flexible for (const d of discriminators) { if (d.kind === 'sizeDiscriminatorNode' && data.length !== d.size) return false; if (d.kind === 'constantDiscriminatorNode') { throw new Error('constantDiscriminatorNode not imeplemented'); } if (d.kind === 'fieldDiscriminatorNode') { const bytes = getFieldBytes(node, d.name, types); const realBytes = data.subarray(d.offset, d.offset + bytes.length); if (!P.utils.equalBytes(bytes, realBytes)) return false; } } return coder.decode(data, opts); }; } function buildDecoder(decoders) { // TODO: P.match? return (data, opts) => { for (const [name, decoder] of Object.entries(decoders)) { const value = decoder(data, opts); if (value !== false) return { TAG: name, data: value }; } throw new Error('Unknown value'); }; } function parseInstructions(instructions, types, pdas, contract) { const encoders = {}; const decoders = {}; const instNames = {}; for (const i of instructions) { if (i.kind !== 'instructionNode') throw new Error('wrong instructionNode'); const args = parseArguments(i.arguments, types); const type = P.struct(args); instNames[i.name] = i; encoders[i.name] = (inst) => { const data = type.encode(inst); const keys = i.accounts.map((i) => ({ address: parseValue(i, inst[i.name], pdas, types), sign: i.isSigner !== false, // either? write: i.isWritable === true, })); if (i.remainingAccounts) { if (i.remainingAccounts.length !== 1) throw new Error('only single remainingAccounts supported'); const r0 = i.remainingAccounts[0]; if (r0.value.kind !== 'argumentValueNode') throw new Error('remainingAccounts: only argumentValueNode supported'); const name = r0.value.name; if (inst[name]) throw new Error('encode: remainingAccounts not implemented'); } return { program: contract, keys, data }; }; decoders[i.name] = decodeDiscriminators(i.discriminators || [], type, i, types); } const decoderData = buildDecoder(decoders); const decoder = (inst, opts) => { if (inst.program !== contract) throw new Error('wrong program address'); const data = decoderData(inst.data, opts); const instMeta = instNames[data.TAG]; const accounts = instMeta.accounts; if (inst.keys.length !== accounts.length) throw new Error('wrong number of accounts'); // if (instMeta.remainingAccounts) { // throw new Error('decode: remainingAccounts not implemented'); // } for (let i = 0; i < accounts.length; i++) { const m = accounts[i]; const r = inst.keys[i]; if (m.isSigner === true && !r.sign) throw new Error('wrong sign flag'); if (m.isWritable === true && !r.write) throw new Error('wrong write flag'); if (r.address !== parseValue(m, undefined, pdas, types)) data.data[m.name] = r.address; } return data; }; return { encoders, decoder }; } export function defineAccounts(accounts, types) { const coders = {}; const decoders = {}; for (const a of accounts) { if (a.kind !== 'accountNode') throw new Error('wrong accountNode'); const type = mapType(a.data, types); // If size not available by coder construction: extract from size discriminator if (type.size === undefined) { for (const d of a.discriminators || []) { if (d.kind !== 'sizeDiscriminatorNode') continue; type.size = d.size; break; } } coders[a.name] = type; decoders[a.name] = decodeDiscriminators(a.discriminators || [], type, a, types); } const decoder = buildDecoder(decoders); return { coders, decoder }; } export function defineProgram(p) { if (p.kind !== 'programNode') throw new Error('idl: wrong program node'); const types = parseDefinedTypes(p.definedTypes); const pdas = parsePDAs(p.publicKey, p.pdas, types); const instructions = parseInstructions(p.instructions, types, pdas, p.publicKey); const accounts = defineAccounts(p.accounts, types); return { name: p.name, contract: p.publicKey, types, accounts, instructions, pdas }; } export function defineIDL(idl) { const res = { [idl.program.name]: { program: defineProgram(idl.program), additionalPrograms: {}, }, }; for (const program of idl.additionalPrograms) res[idl.program.name].additionalPrograms[program.name] = defineProgram(program); return res; } //# sourceMappingURL=index.js.map