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 09d0311..4dbc187 100644 --- a/packages/explorerkit-translator/src/helpers/idl.ts +++ b/packages/explorerkit-translator/src/helpers/idl.ts @@ -40,42 +40,65 @@ export const mapAccountKeysToName = ( } ); }); - const flattenedArray = names.flat(5); - let nonOptionalAccountKeys = 0; - let optionalAccountKeys = 0; - - flattenedArray.forEach((accountKey) => { - if (accountKey.optional) optionalAccountKeys++; - else nonOptionalAccountKeys++; - }); - let optionalKeyCounter = 1; + const flattenedArray = names.flat(5); + const nonOptionalFlattenArray = flattenedArray.filter((accountKey) => !accountKey.optional); let translatedAccountKeysObj: { [name: string]: string; } = {}; - accountKeys.forEach((accountKey, index) => { - const objectKey = flattenedArray[index] ?? { - name: "Unknown", - }; + // If there are lesser accountKeys than optionalAccountKeys, we will just return all the accountKeys as non-optional + // If not, we will treat it as there is a mix of optional and non-optional accounts + if (nonOptionalFlattenArray.length >= accountKeys.length) { + accountKeys.forEach((accountKey, index) => { + let objectKey = nonOptionalFlattenArray[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.name]: mapTypes ? { data: accountKey, type: "publicKey" } : accountKey, + }; - const object: { - [name: string]: string | DataWithMappedType; - } = { - [objectKey.name]: mapTypes ? { data: accountKey, type: "publicKey" } : accountKey, - }; + translatedAccountKeysObj = Object.assign(translatedAccountKeysObj, object); + }); + } else { + let namesIndex = 0; + let optionalKeyCounter = 1; - translatedAccountKeysObj = Object.assign(translatedAccountKeysObj, object); - }); + accountKeys.forEach((accountKey) => { + let objectKey = flattenedArray[namesIndex] ?? { + name: "Unknown", + }; + + if (objectKey.optional) { + // If the accountKeys are lesser than the totalKeys including optional ones, we will treat the current accountKey as a non-optional key and go to the next one + if (accountKeys.length < nonOptionalFlattenArray.length + optionalKeyCounter) { + const nonOptionalIdlAccount = getNonOptionalIdlAccount(namesIndex, flattenedArray); + + if (nonOptionalIdlAccount) { + objectKey = nonOptionalIdlAccount.idlAccount ?? { + name: "Unknown", + }; + namesIndex = nonOptionalIdlAccount.index; + } + } + + optionalKeyCounter++; + } + + const object: { + [name: string]: string | DataWithMappedType; + } = { + [objectKey.name]: mapTypes ? { data: accountKey, type: "publicKey" } : accountKey, + }; + + translatedAccountKeysObj = Object.assign(translatedAccountKeysObj, object); + namesIndex++; + }); + } return translatedAccountKeysObj; } @@ -83,6 +106,36 @@ export const mapAccountKeysToName = ( return {}; }; +export const getNonOptionalIdlAccount: ( + nameIndex: number, + idlAccounts: IdlAccountName[] +) => + | { + idlAccount: IdlAccountName | undefined; + index: number; + } + | undefined = (nameIndex: number, idlAccounts: IdlAccountName[]) => { + let index = nameIndex; + let idlAccount = idlAccounts[index] ?? { + name: "Unknown", + }; + + if (idlAccount) { + if (idlAccount?.optional) { + // Recursively find the next non-optional account + index++; + return getNonOptionalIdlAccount(index, idlAccounts); + } + + return { + idlAccount, + index, + }; + } + + return undefined; +}; + export const extractIdlIxAccountName: (IdlAccount?: IdlAccountItem) => IdlAccountName | IdlAccountName[] | undefined = ( idlAccount ) => { diff --git a/packages/explorerkit-translator/tests/v2/instruction.test.ts b/packages/explorerkit-translator/tests/v2/instruction.test.ts index 40d8598..65b6f57 100644 --- a/packages/explorerkit-translator/tests/v2/instruction.test.ts +++ b/packages/explorerkit-translator/tests/v2/instruction.test.ts @@ -230,7 +230,7 @@ describe("createShankParserWithMappingSupport", () => { }); }); -describe("createAnchorParserWithOptionalAccountKeysSupport", () => { +describe("createShankParserWithOptionalAccountKeysSupport", () => { 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); @@ -263,6 +263,7 @@ describe("createAnchorParserWithOptionalAccountKeysSupport", () => { expect(decodedData).not.toBeNull(); expect(decodedData?.name).toBe("swapBaseIn"); expect(decodedData?.data.ammTargetOrders).toBeUndefined(); + expect(decodedData?.data.poolCoinTokenAccount).toBe("FBba2XsQVhkoQDMfbNLVmo7dsvssdT39BMzVc2eFfE21"); } } });