diff --git a/.changeset/four-cameras-lie.md b/.changeset/four-cameras-lie.md new file mode 100644 index 0000000..752602a --- /dev/null +++ b/.changeset/four-cameras-lie.md @@ -0,0 +1,5 @@ +--- +"@solanafm/explorer-kit": patch +--- + +feat: add support for optional account keys diff --git a/packages/explorerkit-translator/src/helpers/idl.ts b/packages/explorerkit-translator/src/helpers/idl.ts index 23afff7..09d0311 100644 --- a/packages/explorerkit-translator/src/helpers/idl.ts +++ b/packages/explorerkit-translator/src/helpers/idl.ts @@ -14,6 +14,11 @@ export type DataWithMappedType = { data: any; }; +export type IdlAccountName = { + name: string; + optional?: boolean; +}; + export const isDataWithMappedType = (data: any): data is DataWithMappedType => { return data.type && data.data; }; @@ -26,23 +31,47 @@ export const mapAccountKeysToName = ( [name: string]: string | DataWithMappedType; } => { if (idlIxAccounts && accountKeys) { - const names: (string | string[])[] = []; + const names: (IdlAccountName | IdlAccountName[])[] = []; idlIxAccounts.forEach((idlIxAccount) => { - names.push(extractIdlIxAccountName(idlIxAccount) ?? "Unknown"); + names.push( + extractIdlIxAccountName(idlIxAccount) ?? { + name: "Unknown", + } + ); }); - const flattenedArray = names.flat(5); + let nonOptionalAccountKeys = 0; + let optionalAccountKeys = 0; + + flattenedArray.forEach((accountKey) => { + if (accountKey.optional) optionalAccountKeys++; + else nonOptionalAccountKeys++; + }); + + let optionalKeyCounter = 1; + let translatedAccountKeysObj: { [name: string]: string; } = {}; accountKeys.forEach((accountKey, index) => { - const objectKey = flattenedArray[index] ?? ("Unknown" as string); + const objectKey = flattenedArray[index] ?? { + name: "Unknown", + }; + + // If the account is optional, we will check if the accountKeys length is more than the idlIxAccounts length without optional accounts + if (objectKey.optional) { + optionalKeyCounter++; + // If the optional key counter is more than the optional account keys, we will return and not add the name + if (optionalKeyCounter > optionalAccountKeys) return; + if (accountKeys.length > nonOptionalAccountKeys + optionalKeyCounter) return; + } + const object: { [name: string]: string | DataWithMappedType; } = { - [objectKey]: mapTypes ? { data: accountKey, type: "publicKey" } : accountKey, + [objectKey.name]: mapTypes ? { data: accountKey, type: "publicKey" } : accountKey, }; translatedAccountKeysObj = Object.assign(translatedAccountKeysObj, object); @@ -54,49 +83,12 @@ export const mapAccountKeysToName = ( return {}; }; -export const mapAccountKeysToNameV2 = ( - accountKeys?: string[], - idlIxAccounts?: IdlAccountItem[] | IdlInstructionAccount[], - mapTypes?: boolean -): { - [name: string]: string | DataWithMappedType | (string | DataWithMappedType)[]; -} => { - if (!idlIxAccounts || !accountKeys) return {}; - - const names: string[] = idlIxAccounts - .map((idlIxAccount) => extractIdlIxAccountName(idlIxAccount) ?? "Unknown") - .flat(); - let translatedAccountKeysObj: { - [name: string]: string | DataWithMappedType | (string | DataWithMappedType)[]; - } = {}; - - accountKeys.forEach((accountKey, index) => { - const objectKey = names[index] ?? "Unknown"; - const newEntry = mapTypes ? ({ data: accountKey, type: "publicKey" } as DataWithMappedType) : accountKey; - - // If objectKey already exists, we append the new accountKey to the existing array or object - if (objectKey in translatedAccountKeysObj) { - const existingEntry = translatedAccountKeysObj[objectKey]; - - if (Array.isArray(existingEntry)) { - (existingEntry as (string | DataWithMappedType)[]).push(newEntry); - } else { - translatedAccountKeysObj[objectKey] = [existingEntry ?? "", newEntry]; - } - } - // If the objectKey doesn't exist in translatedAccountKeysObj, we just assign it like before - else { - translatedAccountKeysObj[objectKey] = newEntry; - } - }); - - return translatedAccountKeysObj; -}; - -export const extractIdlIxAccountName: (IdlAccount?: IdlAccountItem) => string | string[] | undefined = (idlAccount) => { +export const extractIdlIxAccountName: (IdlAccount?: IdlAccountItem) => IdlAccountName | IdlAccountName[] | undefined = ( + idlAccount +) => { if (idlAccount) { if (isIdlAccounts(idlAccount)) { - const idlIxAccounts: string[] = []; + const idlIxAccounts: IdlAccountName[] = []; idlAccount.accounts.forEach((account) => { let extractedName = extractIdlIxAccountName(account); @@ -104,14 +96,20 @@ export const extractIdlIxAccountName: (IdlAccount?: IdlAccountItem) => string | if (Array.isArray(extractedName)) { extractedName = extractIdlIxAccountName(account); } else { - idlIxAccounts.push(idlAccount.name + "." + extractedName); + idlIxAccounts.push({ + name: idlAccount.name + "." + extractedName.name, + optional: extractedName.optional, + }); } } }); return idlIxAccounts; } else { - return idlAccount.name; + return { + name: idlAccount.name, + optional: idlAccount.isOptional, + }; } } diff --git a/packages/explorerkit-translator/tests/v2/instruction.test.ts b/packages/explorerkit-translator/tests/v2/instruction.test.ts index 1fb7269..40d8598 100644 --- a/packages/explorerkit-translator/tests/v2/instruction.test.ts +++ b/packages/explorerkit-translator/tests/v2/instruction.test.ts @@ -230,6 +230,44 @@ describe("createShankParserWithMappingSupport", () => { }); }); +describe("createAnchorParserWithOptionalAccountKeysSupport", () => { + it("should construct an shank instruction parser for a given valid IDL and map the correct number of account keys with 1 optional account keys", async () => { + const programId = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"; + const idlItem = await getProgramIdl(programId); + const instructionData = "5xcHHuFELpsnhDZW9QA7Vjm"; + + if (idlItem) { + const parser = new SolanaFMParser(idlItem, programId); + const instructionParser = parser.createParser(ParserType.INSTRUCTION); + + if (instructionParser && checkIfInstructionParser(instructionParser)) { + const decodedData = instructionParser.parseInstructions(instructionData, [ + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + "DSUvc5qf5LJHHV5e2tD184ixotSnCnwj7i4jJa4Xsrmt", + "5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1", + "38p42yoKFWgxw2LCbB96wAKa2LwAxiBArY3fc3eA9yWv", + "FBba2XsQVhkoQDMfbNLVmo7dsvssdT39BMzVc2eFfE21", + "GuXKCb9ibwSeRSdSYqaCL3dcxBZ7jJcj6Y7rDwzmUBu9", + "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX", + "58sHobBa2KmyE3EKxCpgxn5KGuzudmGHsgqYgrfciyzd", + "EhboNaGqMiw2rqvh7uN6qEsfxpNoCJQBzPbiiSBNCXmW", + "CnsZUH9AUNFqkEE6oExCevcHkMamPJhPFvND6mLT4ikb", + "33L8Zi2bnkUX99NJeFmSEwYF6DaNknPXTB5EdsVBfb6e", + "Cr278bTbmgyvTbnt1jqCTsPdqUaB9WN3hbGMjRFontmM", + "EYXT9U31MHRsRSBJ8zafg9paUwYLmWZfJHbSwrJ8mNVb", + "B7af1ADihMVF1xE2243G2ggBkLRFTFgHT8hHbjWzqj1F", + "CTyFguG69kwYrzk24P3UuBvY1rR5atu9kf2S6XEwAU8X", + "F8K1h7dk5UGCv4u6w1b6u6WqtjyJfqHcQvN4DT8eYpM8", + "MfDuWeqSHEqTFVYZ7LoexgAK9dxk7cy4DFJWjWMGVWa", + ]); + expect(decodedData).not.toBeNull(); + expect(decodedData?.name).toBe("swapBaseIn"); + expect(decodedData?.data.ammTargetOrders).toBeUndefined(); + } + } + }); +}); + describe("createShankPhoenixParserWithMappingSupport", () => { it("should construct an shank phoenix instruction parser for a given valid IDL and parse the instruction data with types properly mapped according to the idl", async () => { const programId = "PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY";