From 4cefa8bd4f44f6f170faa6fc34e28454ee0a5cf5 Mon Sep 17 00:00:00 2001 From: Noah Gundotra Date: Fri, 13 Sep 2024 10:59:25 -0400 Subject: [PATCH] Prune unused anchor types so layouts can be constructed from legacy IDLs with bad types (#376) Kamino YVaults program was not loading on the explorer with #374 due to bad type "ScopeConversionChain" having undefined type "ScopePriceId" which was not included in the IDL. This PR fixes YVaults IDL loading by pruning unused anchor types. Example tx: - https://explorer.solana.com/tx/3KgAexzvZ8Kdmj1TCkdZ4RCrV5q9ihyZ1xUnXcSCwybybgCupQ8Mf21tZqQWx2eJh45ATZhMWg2FbbzxhYW6UoU6 Example account - https://explorer.solana.com/address/H8h7ZyS5qJR2cwLxvZQdPaNzLik17cRxB5pDvjdXeuBg/anchor-account --- app/providers/anchor.tsx | 2 +- app/utils/convertLegacyIdl.ts | 78 +++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/app/providers/anchor.tsx b/app/providers/anchor.tsx index 6b8d1256..f9dc1cbe 100644 --- a/app/providers/anchor.tsx +++ b/app/providers/anchor.tsx @@ -121,7 +121,7 @@ export function useAnchorProgram(programAddress: string, url: string): { program const program = new Program(formatIdl(idl, programAddress), getProvider(url)); return program; } catch (e) { - console.error('Error creating anchor program', e, { idl }); + console.error('Error creating anchor program for', programAddress, e, { idl }); return null; } }, [idl, programAddress, url]); diff --git a/app/utils/convertLegacyIdl.ts b/app/utils/convertLegacyIdl.ts index 69e4df40..40287932 100644 --- a/app/utils/convertLegacyIdl.ts +++ b/app/utils/convertLegacyIdl.ts @@ -17,6 +17,7 @@ import { IdlType, IdlTypeDef, IdlTypeDefined, + IdlTypeDefTy, } from '@coral-xyz/anchor/dist/cjs/idl'; import { sha256 } from '@noble/hashes/sha256'; import { snakeCase } from 'change-case'; @@ -168,6 +169,78 @@ function convertLegacyIdl(legacyIdl: LegacyIdl, programAddress?: string): Idl { }; } +function traverseType(type: IdlType, refs: Set) { + if (typeof type === 'string') { + // skip + } else if ('vec' in type) { + traverseType(type.vec, refs); + } else if ('option' in type) { + traverseType(type.option, refs); + } else if ('defined' in type) { + refs.add(type.defined.name); + } else if ('array' in type) { + traverseType(type.array[0], refs); + } else if ('generic' in type) { + refs.add(type.generic); + } else if ('coption' in type) { + traverseType(type.coption, refs); + } +} + +function traverseIdlFields(fields: IdlDefinedFields, refs: Set) { + fields.forEach(field => + typeof field === 'string' + ? traverseType(field, refs) + : typeof field === 'object' && 'type' in field + ? traverseType(field.type, refs) + : traverseType(field, refs) + ); +} + +function traverseTypeDef(type: IdlTypeDefTy, refs: Set) { + switch (type.kind) { + case 'struct': + traverseIdlFields(type.fields ?? [], refs); + return; + case 'enum': + type.variants.forEach(variant => traverseIdlFields(variant.fields ?? [], refs)); + return; + case 'type': + traverseType(type.alias, refs); + return; + } +} + +function getTypeReferences(idl: Idl): Set { + const refs = new Set(); + idl.constants?.forEach(constant => traverseType(constant.type, refs)); + idl.accounts?.forEach(account => refs.add(account.name)); + idl.instructions?.forEach(instruction => instruction.args.forEach(arg => traverseType(arg.type, refs))); + idl.events?.forEach(event => refs.add(event.name)); + + // Build up recursive type references in breadth-first manner. + // Very inefficient since we traverse same types multiple times. + // But it works. Open to contributions that do proper graph traversal + let prevSize = refs.size; + let sizeDiff = 1; + while (sizeDiff > 0) { + for (const idlType of idl.types ?? []) { + if (refs.has(idlType.name)) { + traverseTypeDef(idlType.type, refs); + } + } + sizeDiff = refs.size - prevSize; + prevSize = refs.size; + } + return refs; +} + +// Remove types that are not used in definition of instructions, accounts, events, or constants +function removeUnusedTypes(idl: Idl): Idl { + const usedElsewhere = getTypeReferences(idl); + return { ...idl, types: (idl.types ?? []).filter(type => usedElsewhere.has(type.name)) }; +} + function getDisc(prefix: string, name: string): number[] { const hash = sha256(`${prefix}:${name}`); return Array.from(hash.slice(0, 8)); @@ -236,7 +309,7 @@ function convertEnumFields(fields: LegacyEnumFields): IdlDefinedFields { if (Array.isArray(fields) && fields.length > 0 && typeof fields[0] === 'object' && 'type' in fields[0]) { return (fields as LegacyIdlField[]).map(convertField) as IdlField[]; } else { - return (fields as LegacyIdlType[]).map(type => (convertType(type))) as IdlType[]; + return (fields as LegacyIdlType[]).map(type => convertType(type)) as IdlType[]; } } @@ -368,7 +441,6 @@ export function formatIdl(idl: any, programAddress?: string): Idl { throw new Error(`IDL spec not supported: ${spec}`); } } else { - const formattedIdl = convertLegacyIdl(idl as LegacyIdl, programAddress); - return formattedIdl; + return removeUnusedTypes(convertLegacyIdl(idl as LegacyIdl, programAddress)); } }