Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various improvements and bug fixes after testing with mpl-token-metadata client #142

Merged
merged 11 commits into from
Jan 6, 2024
Merged
5 changes: 5 additions & 0 deletions .changeset/eighty-planets-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@metaplex-foundation/kinobi': patch
---

Add fillDefaultPdaSeedValuesVisitor to recursively fill PDA default seed values
5 changes: 5 additions & 0 deletions .changeset/gold-sloths-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@metaplex-foundation/kinobi': patch
---

Support multiple kinds node selectors
5 changes: 5 additions & 0 deletions .changeset/late-papayas-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@metaplex-foundation/kinobi': patch
---

Make setStructDefaultValuesVisitor fill instruction arguments
5 changes: 5 additions & 0 deletions .changeset/red-toys-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@metaplex-foundation/kinobi': patch
---

Bring back seed check in setInstructionAccountDefaultValuesVisitor
27 changes: 1 addition & 26 deletions src/nodes/contextualValueNodes/PdaValueNode.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { isNode } from '../Node';
import { PdaNode } from '../PdaNode';
import { PdaLinkNode, pdaLinkNode } from '../linkNodes';
import { accountValueNode } from './AccountValueNode';
import { argumentValueNode } from './ArgumentValueNode';
import { PdaSeedValueNode, pdaSeedValueNode } from './PdaSeedValueNode';
import { PdaSeedValueNode } from './PdaSeedValueNode';

export type PdaValueNode = {
readonly kind: 'pdaValueNode';
Expand All @@ -23,24 +19,3 @@ export function pdaValueNode(
seeds,
};
}

export function addDefaultSeedValuesFromPdaWhenMissing(
node: PdaNode,
existingSeeds: PdaSeedValueNode[]
): PdaSeedValueNode[] {
const existingSeedNames = new Set(existingSeeds.map((seed) => seed.name));
const defaultSeeds = getDefaultSeedValuesFromPda(node).filter(
(seed) => !existingSeedNames.has(seed.name)
);
return [...defaultSeeds, ...existingSeeds];
}

export function getDefaultSeedValuesFromPda(node: PdaNode): PdaSeedValueNode[] {
return node.seeds.flatMap((seed): PdaSeedValueNode[] => {
if (!isNode(seed, 'variablePdaSeedNode')) return [];
if (isNode(seed.type, 'publicKeyTypeNode')) {
return [pdaSeedValueNode(seed.name, accountValueNode(seed.name))];
}
return [pdaSeedValueNode(seed.name, argumentValueNode(seed.name))];
});
}
2 changes: 1 addition & 1 deletion src/renderers/js-experimental/renderValueNodeVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function renderValueNodeVisitor(input: {
const enumFunction = nameApi.dataEnumFunction(node.enum.name);
const importFrom = node.enum.importFrom ?? 'generatedTypes';

const enumNode = linkables.get(node.enum);
const enumNode = linkables.get(node.enum)?.type;
const isScalar =
enumNode && isNode(enumNode, 'enumTypeNode')
? isScalarEnum(enumNode)
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/js/renderValueNodeVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function renderValueNodeVisitor(input: {
const variantName = pascalCase(node.variant);
const importFrom = node.enum.importFrom ?? 'generatedTypes';

const enumNode = linkables.get(node.enum);
const enumNode = linkables.get(node.enum)?.type;
const isScalar =
enumNode && isNode(enumNode, 'enumTypeNode')
? isScalarEnum(enumNode)
Expand Down
21 changes: 17 additions & 4 deletions src/shared/NodeSelector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Node } from '../nodes';
import { mainCase } from './utils';
import { MainCaseString, mainCase } from './utils';
import type { NodeStack } from './NodeStack';

export type NodeSelector = NodeSelectorPath | NodeSelectorFunction;
Expand All @@ -9,6 +9,7 @@ export type NodeSelector = NodeSelectorPath | NodeSelectorFunction;
* - `*` matches any node.
* - `someText` matches the name of a node, if any.
* - `[someNode]` matches a node of the given kind.
* - `[someNode|someOtherNode]` matches a node with any of the given kind.
* - `[someNode]someText` matches both the kind and the name of a node.
* - `a.b.c` matches a node `c` such that its parent stack contains `a` and `b` in order (but not necessarily subsequent).
*/
Expand All @@ -25,10 +26,22 @@ export const getNodeSelectorFunction = (
if (nodeSelector === '*') return true;
const matches = nodeSelector.match(/^(?:\[([^\]]+)\])?(.*)?$/);
if (!matches) return false;
const [, kind, name] = matches;
if (kind && mainCase(kind) !== node.kind) return false;
if (name && (!('name' in node) || mainCase(name) !== node.name))
const [, kinds, name] = matches;

// Check kinds.
const kindArray = kinds ? kinds.split('|').map(mainCase) : [];
if (
kindArray.length > 0 &&
!kindArray.includes(node.kind as MainCaseString)
) {
return false;
}

// Check names.
if (name && (!('name' in node) || mainCase(name) !== node.name)) {
return false;
}

return true;
};

Expand Down
121 changes: 121 additions & 0 deletions src/visitors/fillDefaultPdaSeedValuesVisitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
INSTRUCTION_INPUT_VALUE_NODE,
InstructionInputValueNode,
InstructionNode,
PdaNode,
PdaSeedValueNode,
accountValueNode,
argumentValueNode,
assertIsNode,
getAllInstructionArguments,
isNode,
isNodeFilter,
pdaSeedValueNode,
pdaValueNode,
} from '../nodes';
import { KinobiError, LinkableDictionary, pipe } from '../shared';
import { extendVisitor } from './extendVisitor';
import { identityVisitor } from './identityVisitor';
import { Visitor } from './visitor';

/**
* Fills in default values for variable PDA seeds that are not explicitly provided.
* Namely, public key seeds are filled with an accountValueNode using the seed name
* and other types of seeds are filled with an argumentValueNode using the seed name.
*
* An instruction and linkable dictionary are required to determine which seeds are
* valids and to find the pdaLinkNode for the seed respectively. Any invalid default
* seed won't be filled in.
*
* Strict mode goes one step further and will throw an error if the final array of
* pdaSeedValueNodes contains invalid seeds or if there aren't enough variable seeds.
*/
export function fillDefaultPdaSeedValuesVisitor(
instruction: InstructionNode,
linkables: LinkableDictionary,
strictMode: boolean = false
) {
return pipe(identityVisitor(INSTRUCTION_INPUT_VALUE_NODE), (v) =>
extendVisitor(v, {
visitPdaValue(node, { next }) {
const visitedNode = next(node);
assertIsNode(visitedNode, 'pdaValueNode');
const foundPda = linkables.get(visitedNode.pda);
if (!foundPda) return visitedNode;
const seeds = addDefaultSeedValuesFromPdaWhenMissing(
instruction,
foundPda,
visitedNode.seeds
);
if (strictMode && !allSeedsAreValid(instruction, foundPda, seeds)) {
throw new KinobiError(
`Invalid seed values for PDA ${foundPda.name} in instruction ${instruction.name}`
);
}
return pdaValueNode(visitedNode.pda, seeds);
},
})
) as Visitor<InstructionInputValueNode, InstructionInputValueNode['kind']>;
}

function addDefaultSeedValuesFromPdaWhenMissing(
instruction: InstructionNode,
pda: PdaNode,
existingSeeds: PdaSeedValueNode[]
): PdaSeedValueNode[] {
const existingSeedNames = new Set(existingSeeds.map((seed) => seed.name));
const defaultSeeds = getDefaultSeedValuesFromPda(instruction, pda).filter(
(seed) => !existingSeedNames.has(seed.name)
);
return [...existingSeeds, ...defaultSeeds];
}

function getDefaultSeedValuesFromPda(
instruction: InstructionNode,
pda: PdaNode
): PdaSeedValueNode[] {
return pda.seeds.flatMap((seed): PdaSeedValueNode[] => {
if (!isNode(seed, 'variablePdaSeedNode')) return [];

const hasMatchingAccount = instruction.accounts.some(
(a) => a.name === seed.name
);
if (isNode(seed.type, 'publicKeyTypeNode') && hasMatchingAccount) {
return [pdaSeedValueNode(seed.name, accountValueNode(seed.name))];
}

const hasMatchingArgument = getAllInstructionArguments(instruction).some(
(a) => a.name === seed.name
);
if (hasMatchingArgument) {
return [pdaSeedValueNode(seed.name, argumentValueNode(seed.name))];
}

return [];
});
}

function allSeedsAreValid(
instruction: InstructionNode,
foundPda: PdaNode,
seeds: PdaSeedValueNode[]
) {
const hasAllVariableSeeds =
foundPda.seeds.filter(isNodeFilter('variablePdaSeedNode')).length ===
seeds.length;
const allAccountsName = instruction.accounts.map((a) => a.name);
const allArgumentsName = getAllInstructionArguments(instruction).map(
(a) => a.name
);
const validSeeds = seeds.every((seed) => {
if (isNode(seed.value, 'accountValueNode')) {
return allAccountsName.includes(seed.value.name);
}
if (isNode(seed.value, 'argumentValueNode')) {
return allArgumentsName.includes(seed.value.name);
}
return true;
});

return hasAllVariableSeeds && validSeeds;
}
1 change: 1 addition & 0 deletions src/visitors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './deduplicateIdenticalDefinedTypesVisitor';
export * from './defaultVisitor';
export * from './deleteNodesVisitor';
export * from './extendVisitor';
export * from './fillDefaultPdaSeedValuesVisitor';
export * from './flattenInstructionDataArgumentsVisitor';
export * from './flattenStructVisitor';
export * from './getByteSizeVisitor';
Expand Down
26 changes: 11 additions & 15 deletions src/visitors/setInstructionAccountDefaultValuesVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import {
InstructionAccountNode,
InstructionNode,
instructionNode,
isNode,
} from '../nodes';
import {
InstructionInputValueNode,
addDefaultSeedValuesFromPdaWhenMissing,
identityValueNode,
payerValueNode,
programIdValueNode,
} from '../nodes/contextualValueNodes';
import { publicKeyValueNode } from '../nodes/valueNodes';
import { LinkableDictionary, mainCase, pipe } from '../shared';
import { extendVisitor } from './extendVisitor';
import { fillDefaultPdaSeedValuesVisitor } from './fillDefaultPdaSeedValuesVisitor';
import { identityVisitor } from './identityVisitor';
import { recordLinkablesVisitor } from './recordLinkablesVisitor';
import { visit } from './visitor';

export type InstructionAccountDefaultRule = {
/** The name of the instruction account or a pattern to match on it. */
Expand Down Expand Up @@ -207,21 +207,17 @@ export function setInstructionAccountDefaultValuesVisitor(
return account;
}

if (isNode(rule.defaultValue, 'pdaValueNode')) {
const foundPda = linkables.get(rule.defaultValue.pda);
const defaultValue = {
...rule.defaultValue,
seeds: foundPda
? addDefaultSeedValuesFromPdaWhenMissing(
foundPda,
rule.defaultValue.seeds
)
: rule.defaultValue.seeds,
try {
return {
...account,
defaultValue: visit(
rule.defaultValue,
fillDefaultPdaSeedValuesVisitor(node, linkables, true)
),
};
return { ...account, defaultValue };
} catch (error) {
return account;
}

return { ...account, defaultValue: rule.defaultValue };
}
);

Expand Down
Loading