From 72c7e09538f38fa98de56caeee6e8c030c8b66b2 Mon Sep 17 00:00:00 2001 From: acheron <98934430+acheroncrypto@users.noreply.github.com> Date: Wed, 25 Dec 2024 13:16:51 +0100 Subject: [PATCH] ts: Fix loading programs with numbers in their names using `workspace` (#3450) --- CHANGELOG.md | 1 + ts/packages/anchor/src/workspace.ts | 47 ++++++++++++++++------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 081be7f9a4..95859c0c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,6 +105,7 @@ The minor version will be incremented upon a breaking change and the patch versi - lang: Deduplicate `zero` accounts against `init` accounts ([#3422](https://github.com/coral-xyz/anchor/pull/3422)). - cli: Fix custom `provider.cluster` ([#3428](https://github.com/coral-xyz/anchor/pull/3428)). - cli: Ignore non semver solana/agave releases to avoid panic ([#3432](https://github.com/coral-xyz/anchor/pull/3432)). +- ts: Fix loading programs with numbers in their names using `workspace` ([#3450](https://github.com/coral-xyz/anchor/pull/3450)). ### Breaking diff --git a/ts/packages/anchor/src/workspace.ts b/ts/packages/anchor/src/workspace.ts index 7ae3e8ed2f..3587bade42 100644 --- a/ts/packages/anchor/src/workspace.ts +++ b/ts/packages/anchor/src/workspace.ts @@ -1,4 +1,5 @@ import * as toml from "toml"; +import camelcase from "camelcase"; import { snakeCase } from "snake-case"; import { Program } from "./program/index.js"; import { isBrowser } from "./utils/common.js"; @@ -19,27 +20,10 @@ const workspace = new Proxy( throw new Error("Workspaces aren't available in the browser"); } - // Converting `programName` to snake_case enables the ability to use any + // Converting `programName` to camelCase enables the ability to use any // of the following to access the workspace program: // `workspace.myProgram`, `workspace.MyProgram`, `workspace["my-program"]`... - programName = snakeCase(programName); - - // Check whether the program name contains any digits - if (/\d/.test(programName)) { - // Numbers cannot be properly converted from camelCase to snake_case, - // e.g. if the `programName` is `myProgram2`, the actual program name could - // be `my_program2` or `my_program_2`. This implementation assumes the - // latter as the default and always converts to `_numbers`. - // - // A solution to the conversion of program names with numbers in them - // would be to always convert the `programName` to camelCase instead of - // snake_case. The problem with this approach is that it would require - // converting everything else e.g. program names in Anchor.toml and IDL - // file names which are both snake_case. - programName = programName - .replace(/\d+/g, (match) => "_" + match) - .replace("__", "_"); - } + programName = camelcase(programName); // Return early if the program is in cache if (workspaceCache[programName]) return workspaceCache[programName]; @@ -50,7 +34,13 @@ const workspace = new Proxy( // Override the workspace programs if the user put them in the config. const anchorToml = toml.parse(fs.readFileSync("Anchor.toml")); const clusterId = anchorToml.provider.cluster; - const programEntry = anchorToml.programs?.[clusterId]?.[programName]; + const programs = anchorToml.programs?.[clusterId]; + let programEntry; + if (programs) { + programEntry = Object.entries(programs).find( + ([key]) => camelcase(key) === programName + )?.[1]; + } let idlPath: string; let programId; @@ -58,7 +48,22 @@ const workspace = new Proxy( idlPath = programEntry.idl; programId = programEntry.address; } else { - idlPath = path.join("target", "idl", `${programName}.json`); + // Assuming the IDL file's name to be the snake_case name of the + // `programName` with `.json` extension results in problems when + // numbers are involved due to the nature of case conversion from + // camelCase to snake_case being lossy. + // + // To avoid the above problem with numbers, read the `idl` directory and + // compare the camelCased version of both file names and `programName`. + const idlDirPath = path.join("target", "idl"); + const fileName = fs + .readdirSync(idlDirPath) + .find((name) => camelcase(path.parse(name).name) === programName); + if (!fileName) { + throw new Error(`Failed to find IDL of program \`${programName}\``); + } + + idlPath = path.join(idlDirPath, fileName); } if (!fs.existsSync(idlPath)) {