UNPKG

micro-sol-signer

Version:

Create, sign & decode Solana transactions with minimum deps

1,207 lines (1,124 loc) 43 kB
import { ed25519 } from '@noble/curves/ed25519'; import { sha256 } from '@noble/hashes/sha2'; import { concatBytes } from '@noble/hashes/utils'; import { base16, base58, base64, utf8 } from '@scure/base'; import * as P from 'micro-packed'; import type { Instruction } from '../index.ts'; /* # 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); export type Bytes = Uint8Array; const b58 = () => { const inner = P.bytes(32); return P.wrap({ size: inner.size, encodeStream: (w: P.Writer, value: string) => inner.encodeStream(w, base58.decode(value)), decodeStream: (r: P.Reader): string => base58.encode(inner.decodeStream(r)), }); }; // first bit -- terminator (1 -- continue, 0 -- last) export const shortU16 = P.wrap({ encodeStream: (w: P.Writer, value: number) => { if (!value) return w.byte(0); for (; value; value >>= 7) { w.bits(value > 0x7f ? 1 : 0, 1); w.bits(value & 0x7f, 7); } }, decodeStream: (r: P.Reader): number => { 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: bigint, b: bigint = ed25519.CURVE.Fp.ORDER) { const res = a % b; return res >= 0n ? res : b + res; } export function isOnCurve(bytes: Bytes | string) { 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.ExtendedPoint.fromHex(modBytes); return true; } catch (e) { return false; } } export function programAddress(program: string, ...seeds: Bytes[]) { let seed = P.utils.concatBytes(...seeds); const noncePos = seed.length; seed = P.utils.concatBytes( seed, new Uint8Array([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'); } type ArrLike<T> = Array<T> | ReadonlyArray<T>; // Boolean based on arbitrary number const numBool: P.Coder<number, boolean> = { encode: (from): boolean => { if (from === 1) return true; if (from === 0) return false; throw new Error('wrong boolean'); }, decode(to: boolean) { if (to === true) return 1; if (to === false) return 0; throw new Error('wrong boolean'); }, }; // Add postfix to string const stringPostfix = (postfix: string): P.Coder<string, string> => ({ 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: P.Coder<number, bigint> = { encode: (from: number): bigint => { if (!Number.isSafeInteger(from)) throw new Error(`expected safe number, got ${typeof from}`); return BigInt(from); }, decode: (to: bigint): number => { 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 = <T>(inner: P.CoderType<T>, value: T): P.CoderType<T | undefined> => P.apply(inner, { encode: (from: T) => from, decode: (to: T | undefined) => (to === undefined ? value : to), }); // TODO: it should be done via flags? function zeroable<T>(inner: P.CoderType<T>): P.CoderType<T | undefined> { 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: T | undefined) { if (value === undefined) w.bytes(ZEROS); else inner.encodeStream(w, value); }, decodeStream: inner.decodeStream, }) as P.CoderType<T | undefined>; } function remainder<T>(inner: P.CoderType<T>): P.CoderType<T | undefined> { return P.wrap({ size: inner.size, encodeStream(w, value: T | undefined) { if (value !== undefined) inner.encodeStream(w, value); }, decodeStream(r) { if (r.isEnd()) return undefined; return inner.decodeStream(r); }, }) as P.CoderType<T | undefined>; } function prefix<T>(inner: P.CoderType<T>, prefix: Uint8Array): P.CoderType<T> { return P.wrap({ size: inner.size, encodeStream(w, value: T) { 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<T>(inner: P.CoderType<T>, postfix: Uint8Array): P.CoderType<T> { return P.wrap({ size: inner.size, encodeStream(w, value: T) { 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<T>( flag: P.CoderType<boolean>, inner: P.CoderType<T> ): P.CoderType<T | undefined> { 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: T | undefined) => { flag.encodeStream(w, !!value); if (value) inner.encodeStream(w, value); else w.bytes(new Uint8Array(inner.size!)); }, decodeStream: (r): T | undefined => { 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; }, }); } // IDL stuff type Node<K extends string, F = {}> = { readonly kind: K } & F; type NumberValue = Node<'numberValueNode', { readonly number: number }>; type NoneValue = Node<'noneValueNode'>; type BytesValue = Node< 'bytesValueNode', { readonly data: string; readonly encoding: 'base16' | 'base58' | 'base64' | 'utf8' } >; type BooleanValue = Node<'booleanValueNode', { readonly boolean: boolean }>; type AccountBumpValue = Node<'accountBumpValueNode', { readonly name: string }>; // ???? type PublicKeyValue = Node<'publicKeyValueNode', { readonly publicKey: string }>; type PayerValue = Node<'payerValueNode'>; type PdaLink = Node<'pdaLinkNode', { readonly name: string }>; type PdaSeedValue = Node< 'pdaSeedValueNode', { readonly name: string; readonly value: Node<'accountValueNode' | 'argumentValueNode', { readonly name: string }>; } >; type PdaValue = Node< 'pdaValueNode', { readonly pda: PdaLink; readonly seeds: ArrLike<PdaSeedValue> } >; type IdentityValue = Node<'identityValueNode'>; // like payer? type AccountValue = Node<'accountValueNode', { readonly name: 'authority' }>; // defaults to another account? type DefaultValue = | NumberValue | NoneValue | AccountBumpValue | BytesValue | BooleanValue | PublicKeyValue | PayerValue | PdaValue | IdentityValue | AccountValue; type DefaultValueMap = { boolean: BooleanValue; number: NumberValue; bytes: BytesValue; none: NoneValue; publicKey: PublicKeyValue; payer: PayerValue; pda: PdaValue; identity: IdentityValue; account: AccountValue; }; type DefaultValueDef<T extends keyof DefaultValueMap> = { defaultValue?: DefaultValueMap[T]; defaultValueStrategy?: 'omitted' | 'optional'; // default: optional }; function parseValueInt< T extends DefaultValue, PT extends ArrLike<PDAType>, DT extends DefinedTypes, >(value: T, _pdas: PDAs<PT, DT>, _dt: 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', ] as const; function parseValue<T extends BasicType, PT extends ArrLike<PDAType>, DT extends DefinedTypes>( node: T, val: any, pdas: PDAs<PT, DT>, dt: 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 as any)(node.defaultValue, pdas, dt); } return val; } // Discriminators type FieldDiscriminator = Node< 'fieldDiscriminatorNode', { readonly name: string; readonly offset: number } >; type SizeDiscriminator = Node<'sizeDiscriminatorNode', { readonly size: number }>; type ConstantDiscriminator = Node< 'constantDiscriminatorNode', { readonly offset: number; readonly constant: ConstantType } >; type Discriminator = SizeDiscriminator | FieldDiscriminator | ConstantDiscriminator; // 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 const; type BigIntCoders = { [K in keyof typeof NumCoders]: (typeof NumCoders)[K]['bigint'] extends true ? K : never; }[keyof typeof NumCoders]; type NumericType = Node< 'numberTypeNode', { readonly format: 'shortU16' | keyof typeof NumCoders; readonly endian?: 'le' | 'be'; } > & DefaultValueDef<'number'>; type GetTypeNumeric<T extends NumericType> = T['format'] extends BigIntCoders ? bigint : number; // As bigint function parseNumeric(type: NumericType) { 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 as P.CoderType<bigint>, { encode: (from) => from, decode(to) { if (typeof to !== 'bigint' && Number.isSafeInteger(to)) return BigInt(to); return to; }, }) as P.CoderType<bigint | number>; } return format; } // As number (for counts). TODO: merge with parseNumeric function parseNumericSafe(type: NumericType): P.CoderType<number> { const t = parseNumeric(type); const isBigint = NumCoders[type.format].bigint; // On read replace bigints with numbers if (isBigint) { return P.apply(t as P.CoderType<bigint | number>, { 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, }) as P.CoderType<number>; } return t as P.CoderType<number>; } type CountType = | Node<'prefixedCountNode', { prefix: NumericType }> | Node<'remainderCountNode'> | Node<'fixedCountNode', { value: number }>; function parseCount(count: CountType): P.Length { 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'); } type EnumVariants = ( | Node<'enumEmptyVariantTypeNode'> | Node<'enumStructVariantTypeNode', { readonly struct: any }> | Node<'enumTupleVariantTypeNode', { readonly tuple: TupleType }> ) & { readonly name: string; readonly discriminator?: number }; type EnumType = Node< 'enumTypeNode', { readonly variants: ArrLike<EnumVariants>; readonly size: NumericType } >; type ArrayType = Node<'arrayTypeNode', { readonly item: BasicType; readonly count: CountType }>; type PublicKeyType = Node<'publicKeyTypeNode'>; type TypeLinkType = Node<'definedTypeLinkNode', { readonly name: string }>; type BooleanType = Node<'booleanTypeNode', { readonly size: NumericType }> & DefaultValueDef<'boolean'>; type StringType = Node<'stringTypeNode'>; type StructField = Node< 'structFieldTypeNode', { readonly name: string; readonly type: BasicType } > & DefaultValueDef<any>; // TODO: fix type StructType = Node<'structTypeNode', { readonly fields: ArrLike<StructField> }>; type OptionalType = Node< 'optionTypeNode', { readonly item: BasicType; readonly prefix?: NumericType; readonly fixed?: boolean } >; type AmountType = Node< 'amountTypeNode', { readonly decimals: number; readonly unit: string; readonly number: NumericType } >; type FixedSizeType = Node<'fixedSizeTypeNode', { readonly size: number; readonly type: BasicType }>; type BytesType = Node<'bytesTypeNode'>; type PrefixType = Node< 'sizePrefixTypeNode', { readonly type: BasicType; readonly prefix: NumericType } >; type ZeroableType = Node<'zeroableOptionTypeNode', { readonly item: BasicType }>; type RemainderOptionType = Node<'remainderOptionTypeNode', { readonly item: BasicType }>; type HiddenPrefixType = Node< 'hiddenPrefixTypeNode', { readonly type: BasicType; readonly prefix: ArrLike<BasicType> } >; type HiddenSuffixType = Node< 'hiddenSuffixTypeNode', { readonly type: BasicType; readonly suffix: ArrLike<BasicType> } >; type ConstantType = Node<'constantValueNode', { readonly type: BasicType; readonly value: any }>; type PreOffsetType = Node< 'preOffsetTypeNode', { readonly offset: number; readonly strategy: 'padded' | 'absolute' | 'relative'; readonly type: BasicType; } >; type PostOffsetType = Node< 'PostOffsetTypeNode', { readonly offset: number; readonly strategy: 'padded' | 'absolute' | 'relative'; readonly type: BasicType; } >; type TupleType = Node<'tupleTypeNode', { readonly items: ArrLike<BasicType> }>; type MapType = Node< 'mapTypeNode', { readonly key: BasicType; readonly value: BasicType; readonly count: CountType; } >; type BasicType = ( | EnumType | NumericType | OptionalType | StructType | ArrayType | PublicKeyType | TypeLinkType | StringType | BooleanType | AmountType | FixedSizeType | BytesType | ZeroableType | PrefixType | RemainderOptionType | ConstantType | PreOffsetType | StructField | HiddenPrefixType | HiddenSuffixType | PostOffsetType | TupleType | MapType ) & { defaultValue?: DefaultValue; defaultValueStrategy?: 'omitted' | 'optional' }; type DefinedTypes = Record<string, P.CoderType<any>>; type GetTypeStruct<T extends StructType, DT extends DefinedTypes = {}> = { [K in T['fields'][number]['name']]: GetType< Extract<T['fields'][number], { name: K }>['type'], DT >; }; type GetTypeTuple<T extends TupleType, DT extends DefinedTypes = {}> = T['items'] extends readonly [ infer A, ] ? [A extends BasicType ? GetType<A, DT> : never] : T['items'] extends readonly [infer A, infer B] ? [A extends BasicType ? GetType<A, DT> : never, B extends BasicType ? GetType<B, DT> : never] : T['items'] extends readonly [infer A, infer B, infer C] ? [ A extends BasicType ? GetType<A, DT> : never, B extends BasicType ? GetType<B, DT> : never, C extends BasicType ? GetType<C, DT> : never, ] : T['items'] extends ReadonlyArray<infer Item> ? (Item extends BasicType ? GetType<Item, DT> : never)[] : never[]; type GetTypeEnum< T extends EnumType, DT extends DefinedTypes = {}, > = T['variants'] extends readonly [] ? never : T['variants'][number] extends infer Variant ? Variant extends Node<'enumEmptyVariantTypeNode'> & { readonly name: infer Name } ? { TAG: Name extends string ? Name : never } : Variant extends Node<'enumStructVariantTypeNode'> & { readonly name: infer Name; readonly struct: infer Struct; } ? { TAG: Name extends string ? Name : never; data: Struct extends StructType ? GetTypeStruct<Struct, DT> : never; } : Variant extends Node<'enumTupleVariantTypeNode'> & { readonly name: infer Name; readonly tuple: infer Tuple; } ? { TAG: Name extends string ? Name : never; data: Tuple extends TupleType ? GetTypeTuple<Tuple, DT> : never; } : never : never; // type TypeLinkType = Node<'definedTypeLinkNode', { readonly name: string }>; type GetTypeLink<T extends TypeLinkType, DT extends DefinedTypes = {}> = T['name'] extends keyof DT ? P.UnwrapCoder<DT[T['name']]> : never; // prettier-ignore type GetTypeBase<T extends BasicType, DT extends DefinedTypes = {}> = // Basic T extends NumericType ? GetTypeNumeric<T> : T extends BooleanType ? boolean : T extends StringType ? string : T extends AmountType ? string : T extends BytesType ? Uint8Array : T extends PublicKeyType ? string : // Structs T extends ArrayType ? (GetType<T['item'], DT>)[] : T extends MapType ? Map<GetType<T['key'], DT>, GetType<T['value'], DT>> : // TODO: fix to map? T extends StructType ? GetTypeStruct<T, DT> : T extends TupleType ? GetTypeTuple<T, DT> : T extends EnumType ? GetTypeEnum<T, DT> : T extends TypeLinkType ? GetTypeLink<T, DT> : // Passhrough T extends FixedSizeType ? GetType<T['type'], DT> : T extends HiddenPrefixType ? GetType<T['type'], DT> : T extends HiddenSuffixType ? GetType<T['type'], DT> : T extends PreOffsetType ? GetType<T['type'], DT> : T extends PostOffsetType ? GetType<T['type'], DT> : T extends ZeroableType ? GetType<T['item'], DT> : T extends PrefixType ? GetType<T['type'], DT> : T extends ConstantType ? GetType<T['type'], DT> : T extends RemainderOptionType ? GetType<T['item'], DT> | undefined : T extends OptionalType ? GetType<T['item'], DT> | undefined : unknown; // default export type GetType<T extends BasicType, DT extends DefinedTypes = {}> = T extends { defaultValue: Exclude<DefaultValue, { kind: (typeof IGNORE_DEFAULT)[number] }>; defaultValueStrategy?: infer Strategy; } ? Strategy extends 'omitted' ? undefined : Strategy extends 'optional' | undefined ? GetTypeBase<T, DT> | undefined : GetTypeBase<T, DT> : // If no defaultValue or it's an ignored kind, proceed as normal GetTypeBase<T, DT>; const types: Record<string, (type: any, dt: DefinedTypes) => P.CoderType<any>> = { // Primitive publicKeyTypeNode: () => pubKey, numberTypeNode: (type: NumericType) => parseNumeric(type), booleanTypeNode: (type: BooleanType) => P.apply(parseNumericSafe(type.size), numBool), bytesTypeNode: (_type: BytesType) => P.bytes(null), // Strip zero bytes from string: ugly, but required for compatibility with solana utf8 coder stringTypeNode: (_type: StringType) => P.validate(P.string(null), (s) => s.replace(/\u0000/g, '')), amountTypeNode: (type: AmountType) => { let x = parseNumeric(type.number) as any; 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: FixedSizeType, dt: DefinedTypes = {}) => P.prefix(type.size, (mapType as any)(type.type, dt)), sizePrefixTypeNode: (type: PrefixType, dt: DefinedTypes = {}) => P.prefix(parseNumericSafe(type.prefix), (mapType as any)(type.type, dt)), optionTypeNode: (type: OptionalType, dt: DefinedTypes = {}) => { const inner = (mapType as any)(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: ArrayType, dt: DefinedTypes = {}) => P.array(parseCount(type.count), (mapType as any)(type.item, dt)), enumVariant: (type: EnumVariants, dt: DefinedTypes = {}) => { if (type.kind === 'enumStructVariantTypeNode') return (mapType as any)(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: EnumType, dt: DefinedTypes = {}) => { 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 as any); }, mapTypeNode: (type: MapType, dt: DefinedTypes = {}) => { const inner = P.tuple([(mapType as any)(type.key, dt), (mapType as any)(type.value, dt)]); const lst = P.array(parseCount(type.count), inner); return P.apply(lst, P.coders.dict()); }, structFieldTypeNode: (type: StructField, dt: DefinedTypes = {}) => (mapType as any)( { ...type.type, defaultValue: type.defaultValue, defaultValueStrategy: type.defaultValueStrategy, }, dt ), structTypeNode: <T extends StructType, DT extends DefinedTypes>( type: T, dt: DT ): P.CoderType<GetTypeStruct<T, DT>> => P.struct( Object.fromEntries( type.fields.map((i) => { if (i.kind !== 'structFieldTypeNode') throw new Error('wrong structFieldTypeNode'); return [i.name, mapType(i, dt)]; }) ) as any ) as any, tupleTypeNode: (type: TupleType, dt: DefinedTypes = {}) => P.tuple(type.items.map((i) => (mapType as any)(i, dt))), definedTypeLinkNode: (type: DefinedType, dt: DefinedTypes = {}) => { return P.lazy(() => { if (!dt[type.name]) throw new Error('unknown type: ' + type.name); return dt[type.name]; }); }, zeroableOptionTypeNode: <T extends ZeroableType>(type: T, dt: DefinedTypes = {}) => zeroable((mapType as any)(type.item, dt)), remainderOptionTypeNode: <T extends RemainderOptionType>(type: T, dt: DefinedTypes = {}) => remainder((mapType as any)(type.item, dt)), constantValueNode: <T extends ConstantType>(type: T, dt: DefinedTypes = {}) => P.magic((mapType as any)(type.type, dt), parseValueInt(type.value, {}, dt)), hiddenPrefixTypeNode: <T extends HiddenPrefixType>(type: T, dt: DefinedTypes = {}) => { return prefix( (mapType as any)(type.type, dt), concatBytes(...type.prefix.map((i) => (mapType as any)(i, dt).encode())) ); }, hiddenSuffixTypeNode: <T extends HiddenSuffixType>(type: T, dt: DefinedTypes = {}) => postfix( (mapType as any)(type.type, dt), concatBytes(...type.suffix.map((i) => (mapType as any)(i, dt).encode())) ), preOffsetTypeNode: <T extends PreOffsetType>(type: T, dt: DefinedTypes = {}) => { if (type.strategy === 'padded') return prefix((mapType as any)(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: <T extends PreOffsetType>(type: T, dt: DefinedTypes = {}) => { if (type.strategy === 'padded') return postfix((mapType as any)(type.type, dt), new Uint8Array(type.offset)); throw new Error('not implemented'); }, }; function mapTypeInternal(type: BasicType, definedTypes: DefinedTypes = {}): any { const t = (types as any)[type.kind]; if (t === undefined) throw new Error('Unknown type: ' + type.kind); return t(type, definedTypes); } export function mapType<T extends BasicType, DT extends DefinedTypes>( type: T, dt: DT ): GetType<T, 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 as any) ) { const def = parseValueInt(type.defaultValue, {}, dt); if (type.defaultValueStrategy === 'omitted') return P.magic(t, def) as any; if (type.defaultValueStrategy === 'optional' || type.defaultValueStrategy === undefined) return defaultCoder(t, def) as any; throw new Error('wrong defaultValueStrategy: ' + type.defaultValueStrategy); } return t; } type DefinedType = { readonly kind: 'definedTypeNode'; readonly name: string; readonly type: BasicType; }; export type GetDefinedTypes<T extends ArrLike<DefinedType>> = { [K in T[number]['name']]: P.CoderType<GetType<Extract<T[number], { name: K }>['type']>>; }; function parseDefinedTypes<T extends ArrLike<DefinedType>>(types: T): GetDefinedTypes<T> { const res: Record<string, any> = {}; // Disable recursive stuff here for (const t of types) res[t.name] = (mapType as any)(t.type, res); return res as any; } type PDASeeds = | Node<'variablePdaSeedNode', { readonly name: string; readonly type: BasicType }> | Node< 'constantPdaSeedNode', { readonly name: string; readonly type: BasicType; readonly value: DefaultValue } >; type PDAType = Node<'pdaNode', { readonly name: string; readonly seeds: ArrLike<PDASeeds> }>; type GetPDASeeds<T extends PDAType, DT extends DefinedTypes = {}> = { [K in Extract<T['seeds'][number], { name: string }>['name']]: Extract< T['seeds'][number], { name: K } > extends Node<'variablePdaSeedNode', { readonly type: infer Type }> ? GetType<Type & BasicType, DT> : Extract<T['seeds'][number], { name: K }> extends Node< 'constantPdaSeedNode', { readonly type: infer Type } > ? GetType<Type & BasicType, DT> : never; }; // Then, define the return type for parsePDAs type PDAs<T extends ArrLike<PDAType>, DT extends DefinedTypes = {}> = { [K in T[number]['name']]: (value: GetPDASeeds<Extract<T[number], { name: K }>, DT>) => string; }; export function parsePDAs<T extends ArrLike<PDAType>, DT extends DefinedTypes = {}>( program: string, pda: T, dt: DT = {} as DT ): PDAs<T, DT> { const res: Record<string, any> = {}; for (const p of pda) { const fields = Object.fromEntries( p.seeds.map((seed) => { if (seed.kind === 'variablePdaSeedNode') return [seed.name, (mapType as any)(seed.type, dt)]; if (seed.kind === 'constantPdaSeedNode') { // TODO: check return [ seed.name, P.magic((mapType as any)(seed.type, dt), parseValueInt(seed.value, res, dt)), ]; } throw new Error('unknown seed type'); }) ); const coder = P.struct(fields); res[p.name] = (value: any) => programAddress(program, coder.encode(value)); } return res as any; } type Account = Node< 'instructionAccountNode', { readonly name: string; readonly isWritable: boolean; readonly isSigner: boolean | 'either'; readonly isOptional: boolean; } > & DefaultValueDef<'publicKey' | 'pda' | 'identity' | 'payer' | 'account'>; type Argument = Node< 'instructionArgumentNode', { readonly name: string; readonly type: BasicType } > & DefaultValueDef<any>; type GetArgumentType<A extends Argument, DT extends DefinedTypes = {}> = A extends { type: infer T extends BasicType; defaultValue: infer DV; defaultValueStrategy: infer DVS; } ? GetType< { defaultValue: DV; defaultValueStrategy: DVS; } & T, DT > : A extends { type: infer T extends BasicType; defaultValue: infer DV; } ? GetType< { defaultValue: DV; defaultValueStrategy: undefined; } & T, DT > : A extends { type: infer T extends BasicType; } ? GetType<T, DT> : unknown; export type GetTypeArguments<T extends ArrLike<Argument>, DT extends DefinedTypes = {}> = { [K in Extract<T[number], { name: string }>['name']]: GetArgumentType< Extract<T[number], { name: K }>, DT >; }; function parseArguments<T extends ArrLike<Argument>, DT extends DefinedTypes>( args: T, types: DT ): GetTypeArguments<T, DT> { const res: Record<string, any> = {}; for (const a of args) { if (a.kind !== 'instructionArgumentNode') throw new Error('instructionArgumentNode'); const type = (mapType as any)( { ...a.type, defaultValue: a.defaultValue, defaultValueStrategy: a.defaultValueStrategy }, types ); res[a.name] = type; } return res as GetTypeArguments<T, DT>; } function getFieldBytes(node: any, field: string, types: DefinedTypes) { if (node.kind === 'accountNode') { if (node.data.kind === 'structTypeNode') { for (const f of node.data.fields) { if (f.name !== field) continue; return (mapType as any)(f, types).encode(undefined); } } } if (node.kind === 'instructionNode') { for (const f of node.arguments) { if (f.name !== field) continue; return (mapType as any)( { ...f.type, defaultValue: f.defaultValue, defaultValueStrategy: f.defaultValueStrategy }, types ).encode(undefined); } } throw new Error('getFieldBytes wrong node type: ' + node.kind); } function decodeDiscriminators( discriminators: ArrLike<Discriminator>, coder: any, node: any, types: DefinedTypes ) { return (data: Uint8Array, opts?: P.ReaderOpts) => { // 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<T extends Record<string, (data: Uint8Array, opts?: P.ReaderOpts) => any>>( decoders: T ) { // TODO: P.match? return (data: Uint8Array, opts?: P.ReaderOpts) => { 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'); }; } type RemainingAccounts = Node< 'instructionRemainingAccountsNode', { readonly value: Node<'argumentValueNode', { readonly name: string }> } >; type ProgramInstruction = Node< 'instructionNode', { readonly accounts: ArrLike<Account>; readonly arguments: ArrLike<Argument>; readonly discriminators?: ArrLike<Discriminator>; readonly remainingAccounts?: ArrLike<RemainingAccounts>; readonly name: string; readonly optionalAccountStrategy?: 'programId' | 'omitted'; } >; export type GetTypeAccounts<T extends ArrLike<Account>> = { [K in Extract<T[number], { name: string }>['name']]: Extract<T[number], { name: K }> extends { defaultValue?: { kind: 'publicKeyValueNode' }; } ? undefined // Account with publicKeyValueNode default is optional : string; // All other accounts are required }; export type Nullable<T> = // Pick all non-undefinable keys as required properties { [K in keyof T as undefined extends T[K] ? never : K]: T[K]; } & { // Pick all undefinable keys as optional properties (without undefined in their type) [K in keyof T as undefined extends T[K] ? K : never]?: Exclude<T[K], undefined>; }; export type GetInstructionArgs< T extends ProgramInstruction, DT extends DefinedTypes = {}, > = Nullable<GetTypeArguments<T['arguments'], DT> & GetTypeAccounts<T['accounts']>>; type DecodedInstruction<T extends ProgramInstruction, DT extends DefinedTypes = {}> = { TAG: string; data: GetInstructionArgs<T, DT> & { [K in Extract<T['accounts'][number], { name: string }>['name']]?: string; }; }; export type ParsedInstructions< T extends ArrLike<ProgramInstruction>, DT extends DefinedTypes = {}, > = { encoders: { [K in T[number]['name']]: ( inst: GetInstructionArgs<Extract<T[number], { name: K }>, DT> ) => Instruction; }; decoder: (inst: Instruction, opts?: P.ReaderOpts) => DecodedInstruction<T[number], DT>; }; function parseInstructions< T extends ArrLike<ProgramInstruction>, P extends PDAs<any, DT>, DT extends DefinedTypes, >(instructions: T, types: DT, pdas: P, contract: string): ParsedInstructions<T, DT> { const encoders: Record<string, any> = {}; const decoders: Record<string, any> = {}; const instNames: Record<string, ProgramInstruction> = {}; for (const i of instructions) { if (i.kind !== 'instructionNode') throw new Error('wrong instructionNode'); const args = parseArguments(i.arguments, types) as any; const type = P.struct(args); instNames[i.name] = i; encoders[i.name] = (inst: any): Instruction => { const data = type.encode(inst); const keys = i.accounts.map((i) => ({ address: (parseValue as any)(i, inst[i.name], pdas, types) as string, 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: Instruction, opts?: P.ReaderOpts) => { 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 as any)(m, undefined, pdas, types)) data.data[m.name] = r.address; } return data; }; return { encoders, decoder } as any; } type ContractAccount = { readonly kind: 'accountNode'; readonly name: string; readonly data: BasicType; readonly discriminators?: ArrLike<Discriminator>; }; type DecodedAccount<T extends ArrLike<ContractAccount>, DT extends DefinedTypes = {}> = { [K in T[number]['name']]: { TAG: K; data: GetType<Extract<T[number], { name: K }>['data'], DT>; }; }[T[number]['name']]; export type AccountDefinitions<T extends ArrLike<ContractAccount>, DT extends DefinedTypes = {}> = { coders: { [K in T[number]['name']]: P.CoderType<GetType<Extract<T[number], { name: K }>['data'], DT>>; }; decoder: (data: Uint8Array, opts?: P.ReaderOpts) => DecodedAccount<T, DT>; }; export function defineAccounts<T extends ArrLike<ContractAccount>, DT extends DefinedTypes>( accounts: T, types: DT ): AccountDefinitions<T, DT> { const coders: Record<string, any> = {}; const decoders: Record<string, any> = {}; for (const a of accounts) { if (a.kind !== 'accountNode') throw new Error('wrong accountNode'); const type = (mapType as any)(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 } as any; } type Program = { readonly kind: 'programNode'; readonly name: string; readonly publicKey: string; readonly definedTypes: ArrLike<DefinedType>; readonly pdas: ArrLike<PDAType>; readonly instructions: ArrLike<ProgramInstruction>; readonly accounts: ArrLike<ContractAccount>; }; type GetTypeProgram<P extends Program> = { name: P['name']; contract: P['publicKey']; types: GetDefinedTypes<P['definedTypes']>; pdas: PDAs<P['pdas'], GetDefinedTypes<P['definedTypes']>>; instructions: ParsedInstructions<P['instructions'], GetDefinedTypes<P['definedTypes']>>; accounts: AccountDefinitions<P['accounts'], GetDefinedTypes<P['definedTypes']>>; }; export function defineProgram<P extends Program>(p: P): GetTypeProgram<P> { if (p.kind !== 'programNode') throw new Error('idl: wrong program node'); const types = parseDefinedTypes(p.definedTypes) as any; const pdas = parsePDAs(p.publicKey, p.pdas, types); const instructions = (parseInstructions as any)(p.instructions, types, pdas, p.publicKey); const accounts = defineAccounts(p.accounts, types); return { name: p.name, contract: p.publicKey, types, accounts, instructions, pdas } as any; } type IDL = { readonly kind: 'rootNode'; readonly program: Program; readonly additionalPrograms: ArrLike<Program>; }; type GetTypeIDL<T extends IDL> = { [P in T['program']['name']]: { program: GetTypeProgram<T['program']>; additionalPrograms: { [K in T['additionalPrograms'][number]['name']]: GetTypeProgram< Extract<T['additionalPrograms'][number], { name: K }> >; }; }; }; export function defineIDL<T extends IDL>(idl: T): GetTypeIDL<T> { const res: Record<string, any> = { [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 as GetTypeIDL<T>; }