micro-sol-signer
Version:
Create, sign & decode Solana transactions with minimum deps
664 lines (657 loc) • 25.6 kB
JavaScript
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