Skip to content

Commit a644f59

Browse files
authored
feat: Auto-extract inline PDAs from v01 Anchor instruction accounts (#984)
1 parent af3f091 commit a644f59

File tree

5 files changed

+446
-0
lines changed

5 files changed

+446
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codama/nodes-from-anchor': minor
3+
---
4+
5+
Auto-extract inline PDAs from v01 Anchor instruction accounts into program-level PdaNodes

packages/nodes-from-anchor/src/defaultVisitor.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
Visitor,
1313
} from '@codama/visitors';
1414

15+
import { extractPdasVisitor } from './extractPdasVisitor';
16+
1517
export function defaultVisitor() {
1618
return rootNodeVisitor(currentRoot => {
1719
let root: RootNode = currentRoot;
@@ -21,6 +23,9 @@ export function defaultVisitor() {
2123
root = newRoot;
2224
};
2325

26+
// PDAs.
27+
updateRoot(extractPdasVisitor());
28+
2429
// Defined types.
2530
updateRoot(deduplicateIdenticalDefinedTypesVisitor());
2631

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { logWarn } from '@codama/errors';
2+
import {
3+
assertIsNode,
4+
camelCase,
5+
type CamelCaseString,
6+
instructionAccountNode,
7+
type InstructionNode,
8+
instructionNode,
9+
isNode,
10+
pdaLinkNode,
11+
type PdaNode,
12+
pdaNode,
13+
type ProgramNode,
14+
programNode,
15+
} from '@codama/nodes';
16+
import { bottomUpTransformerVisitor, getUniqueHashStringVisitor, visit, type Visitor } from '@codama/visitors';
17+
18+
type Fingerprint = string;
19+
20+
function pdaFingerprint(pda: PdaNode, hashVisitor: Visitor<string>): Fingerprint {
21+
return visit(pdaNode({ ...pda, name: '' }), hashVisitor);
22+
}
23+
24+
function getUniquePdaName(name: CamelCaseString, usedNames: Set<CamelCaseString>): CamelCaseString {
25+
if (!usedNames.has(name)) return name;
26+
let suffix = 2;
27+
let candidate = camelCase(`${name}${suffix}`);
28+
while (usedNames.has(candidate)) {
29+
suffix++;
30+
candidate = camelCase(`${name}${suffix}`);
31+
}
32+
return candidate;
33+
}
34+
35+
export function extractPdasVisitor() {
36+
return bottomUpTransformerVisitor([
37+
{
38+
select: '[programNode]',
39+
transform: node => {
40+
assertIsNode(node, 'programNode');
41+
return extractPdasFromProgram(node);
42+
},
43+
},
44+
]);
45+
}
46+
47+
export function extractPdasFromProgram(program: ProgramNode): ProgramNode {
48+
const hashVisitor = getUniqueHashStringVisitor();
49+
const pdaMap = new Map<Fingerprint, PdaNode>();
50+
const usedNames = new Set<CamelCaseString>(program.pdas.map(p => p.name));
51+
const nameToFingerprint = new Map<CamelCaseString, Fingerprint>();
52+
53+
const rewrittenInstructions = program.instructions.map(instruction => {
54+
const rewrittenAccounts = instruction.accounts.map(account => {
55+
if (
56+
!account.defaultValue ||
57+
!isNode(account.defaultValue, 'pdaValueNode') ||
58+
!isNode(account.defaultValue.pda, 'pdaNode')
59+
) {
60+
return account;
61+
}
62+
63+
const pda = account.defaultValue.pda;
64+
if (pda.programId && pda.programId !== program.publicKey) return account;
65+
66+
const fingerprint = pdaFingerprint(pda, hashVisitor);
67+
68+
if (!pdaMap.has(fingerprint)) {
69+
let resolvedName = pda.name;
70+
const existingFingerprint = nameToFingerprint.get(resolvedName);
71+
72+
if (existingFingerprint !== undefined && existingFingerprint !== fingerprint) {
73+
resolvedName = camelCase(`${instruction.name}_${pda.name}`);
74+
logWarn(
75+
`PDA name collision: "${pda.name}" has different seeds across instructions. ` +
76+
`Renaming to "${resolvedName}".`,
77+
);
78+
}
79+
80+
resolvedName = getUniquePdaName(resolvedName, usedNames);
81+
82+
usedNames.add(resolvedName);
83+
nameToFingerprint.set(resolvedName, fingerprint);
84+
pdaMap.set(fingerprint, pdaNode({ ...pda, name: resolvedName }));
85+
}
86+
87+
const extractedPda = pdaMap.get(fingerprint)!;
88+
const defaultValue = { ...account.defaultValue, pda: pdaLinkNode(extractedPda.name) };
89+
return instructionAccountNode({ ...account, defaultValue });
90+
});
91+
92+
return instructionNode({
93+
...instruction,
94+
accounts: rewrittenAccounts,
95+
}) as InstructionNode;
96+
});
97+
98+
return programNode({
99+
...program,
100+
instructions: rewrittenInstructions,
101+
pdas: [...program.pdas, ...pdaMap.values()],
102+
});
103+
}

packages/nodes-from-anchor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IdlV01, rootNodeFromAnchorV01 } from './v01';
77

88
export * from './defaultVisitor';
99
export * from './discriminators';
10+
export * from './extractPdasVisitor';
1011
export * from './v00';
1112
export * from './v01';
1213

0 commit comments

Comments
 (0)