Skip to content

Commit 5bad9f1

Browse files
authored
fix: impl nested account name deduplication (#953)
* fix: impl nested account name deduplication Signed-off-by: AvhiMaz <[email protected]> * fix: enhance nested account name deduplication Signed-off-by: AvhiMaz <[email protected]> * fix: add tests for depth-2 nested accounts Signed-off-by: AvhiMaz <[email protected]> --------- Signed-off-by: AvhiMaz <[email protected]>
1 parent 8216b38 commit 5bad9f1

File tree

6 files changed

+288
-16
lines changed

6 files changed

+288
-16
lines changed

.changeset/orange-llamas-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codama/nodes-from-anchor': patch
3+
---
4+
5+
Prevent duplicate account names in nested groups by applying conditional prefixing
Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,54 @@
1-
import { InstructionAccountNode, instructionAccountNode } from '@codama/nodes';
1+
import { camelCase, InstructionAccountNode, instructionAccountNode } from '@codama/nodes';
22

33
import { IdlV00Account, IdlV00AccountItem } from './idl';
44

5-
export function instructionAccountNodesFromAnchorV00(idl: IdlV00AccountItem[]): InstructionAccountNode[] {
5+
function hasDuplicateAccountNames(idl: IdlV00AccountItem[]): boolean {
6+
const seenNames = new Set<string>();
7+
8+
function checkDuplicates(items: IdlV00AccountItem[]): boolean {
9+
for (const item of items) {
10+
if ('accounts' in item) {
11+
if (checkDuplicates(item.accounts)) {
12+
return true;
13+
}
14+
} else {
15+
const name = camelCase(item.name ?? '');
16+
if (seenNames.has(name)) {
17+
return true;
18+
}
19+
seenNames.add(name);
20+
}
21+
}
22+
return false;
23+
}
24+
25+
return checkDuplicates(idl);
26+
}
27+
28+
export function instructionAccountNodesFromAnchorV00(
29+
idl: IdlV00AccountItem[],
30+
prefix?: string,
31+
): InstructionAccountNode[] {
32+
const shouldPrefix = prefix !== undefined || hasDuplicateAccountNames(idl);
33+
634
return idl.flatMap(account =>
735
'accounts' in account
8-
? instructionAccountNodesFromAnchorV00(account.accounts)
9-
: [instructionAccountNodeFromAnchorV00(account)],
36+
? instructionAccountNodesFromAnchorV00(
37+
account.accounts,
38+
shouldPrefix ? (prefix ? `${prefix}_${account.name}` : account.name) : undefined,
39+
)
40+
: [instructionAccountNodeFromAnchorV00(account, shouldPrefix ? prefix : undefined)],
1041
);
1142
}
1243

13-
export function instructionAccountNodeFromAnchorV00(idl: IdlV00Account): InstructionAccountNode {
44+
export function instructionAccountNodeFromAnchorV00(idl: IdlV00Account, prefix?: string): InstructionAccountNode {
1445
const isOptional = idl.optional ?? idl.isOptional ?? false;
1546
const desc = idl.desc ? [idl.desc] : undefined;
1647
return instructionAccountNode({
1748
docs: idl.docs ?? desc ?? [],
1849
isOptional,
1950
isSigner: idl.isOptionalSigner ? 'either' : (idl.isSigner ?? false),
2051
isWritable: idl.isMut ?? false,
21-
name: idl.name ?? '',
52+
name: prefix ? `${prefix}_${idl.name ?? ''}` : (idl.name ?? ''),
2253
});
2354
}

packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
AccountValueNode,
33
ArgumentValueNode,
4+
camelCase,
45
InstructionAccountNode,
56
instructionAccountNode,
67
InstructionArgumentNode,
@@ -17,26 +18,57 @@ import {
1718
import { IdlV01InstructionAccount, IdlV01InstructionAccountItem, IdlV01Seed } from './idl';
1819
import { pdaSeedNodeFromAnchorV01 } from './PdaSeedNode';
1920

21+
function hasDuplicateAccountNames(idl: IdlV01InstructionAccountItem[]): boolean {
22+
const seenNames = new Set<string>();
23+
24+
function checkDuplicates(items: IdlV01InstructionAccountItem[]): boolean {
25+
for (const item of items) {
26+
if ('accounts' in item) {
27+
if (checkDuplicates(item.accounts)) {
28+
return true;
29+
}
30+
} else {
31+
const name = camelCase(item.name ?? '');
32+
if (seenNames.has(name)) {
33+
return true;
34+
}
35+
seenNames.add(name);
36+
}
37+
}
38+
return false;
39+
}
40+
41+
return checkDuplicates(idl);
42+
}
43+
2044
export function instructionAccountNodesFromAnchorV01(
2145
idl: IdlV01InstructionAccountItem[],
2246
instructionArguments: InstructionArgumentNode[],
47+
prefix?: string,
2348
): InstructionAccountNode[] {
49+
const shouldPrefix = prefix !== undefined || hasDuplicateAccountNames(idl);
50+
2451
return idl.flatMap(account =>
2552
'accounts' in account
26-
? instructionAccountNodesFromAnchorV01(account.accounts, instructionArguments)
27-
: [instructionAccountNodeFromAnchorV01(account, instructionArguments)],
53+
? instructionAccountNodesFromAnchorV01(
54+
account.accounts,
55+
instructionArguments,
56+
shouldPrefix ? (prefix ? `${prefix}_${account.name}` : account.name) : undefined,
57+
)
58+
: [instructionAccountNodeFromAnchorV01(account, instructionArguments, shouldPrefix ? prefix : undefined)],
2859
);
2960
}
3061

3162
export function instructionAccountNodeFromAnchorV01(
3263
idl: IdlV01InstructionAccount,
3364
instructionArguments: InstructionArgumentNode[],
65+
prefix?: string,
3466
): InstructionAccountNode {
3567
const isOptional = idl.optional ?? false;
3668
const docs = idl.docs ?? [];
3769
const isSigner = idl.signer ?? false;
3870
const isWritable = idl.writable ?? false;
39-
const name = idl.name ?? '';
71+
const name = prefix ? `${prefix}_${idl.name ?? ''}` : (idl.name ?? '');
4072
let defaultValue: PdaValueNode | PublicKeyValueNode | undefined;
4173

4274
if (idl.address) {
@@ -48,7 +80,7 @@ export function instructionAccountNodeFromAnchorV01(
4880
if (!seedsWithNestedPaths) {
4981
const [seedDefinitions, seedValues] = idl.pda.seeds.reduce(
5082
([seeds, lookups], seed: IdlV01Seed) => {
51-
const { definition, value } = pdaSeedNodeFromAnchorV01(seed, instructionArguments);
83+
const { definition, value } = pdaSeedNodeFromAnchorV01(seed, instructionArguments, prefix);
5284
return [[...seeds, definition], value ? [...lookups, value] : lookups];
5385
},
5486
<[PdaSeedNode[], PdaSeedValueNode[]]>[[], []],
@@ -57,7 +89,7 @@ export function instructionAccountNodeFromAnchorV01(
5789
let programId: string | undefined;
5890
let programIdValue: AccountValueNode | ArgumentValueNode | undefined;
5991
if (idl.pda.program !== undefined) {
60-
const { definition, value } = pdaSeedNodeFromAnchorV01(idl.pda.program, instructionArguments);
92+
const { definition, value } = pdaSeedNodeFromAnchorV01(idl.pda.program, instructionArguments, prefix);
6193
if (
6294
isNode(definition, 'constantPdaSeedNode') &&
6395
isNode(definition.value, 'bytesValueNode') &&

packages/nodes-from-anchor/src/v01/PdaSeedNode.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { IdlV01Seed } from './idl';
2424
export function pdaSeedNodeFromAnchorV01(
2525
seed: IdlV01Seed,
2626
instructionArguments: InstructionArgumentNode[],
27+
prefix?: string,
2728
): Readonly<{ definition: PdaSeedNode; value?: PdaSeedValueNode }> {
2829
const kind = seed.kind;
2930

@@ -35,9 +36,10 @@ export function pdaSeedNodeFromAnchorV01(
3536
case 'account': {
3637
// Ignore nested paths.
3738
const [accountName] = seed.path.split('.');
39+
const prefixedAccountName = prefix ? `${prefix}_${accountName}` : accountName;
3840
return {
39-
definition: variablePdaSeedNode(accountName, publicKeyTypeNode()),
40-
value: pdaSeedValueNode(accountName, accountValueNode(accountName)),
41+
definition: variablePdaSeedNode(prefixedAccountName, publicKeyTypeNode()),
42+
value: pdaSeedValueNode(prefixedAccountName, accountValueNode(prefixedAccountName)),
4143
};
4244
}
4345
case 'arg': {

packages/nodes-from-anchor/test/v00/InstructionAccountNode.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ test('it creates instruction account nodes', () => {
2323
);
2424
});
2525

26-
test('it flattens nested instruction accounts', () => {
26+
test('it flattens nested instruction accounts without prefixing when no duplicates exist', () => {
2727
const nodes = instructionAccountNodesFromAnchorV00([
2828
{ isMut: false, isSigner: false, name: 'accountA' },
2929
{
@@ -43,3 +43,29 @@ test('it flattens nested instruction accounts', () => {
4343
instructionAccountNode({ isSigner: true, isWritable: true, name: 'accountD' }),
4444
]);
4545
});
46+
47+
test('it prevents duplicate names by prefixing nested accounts with different parent names', () => {
48+
const nodes = instructionAccountNodesFromAnchorV00([
49+
{
50+
accounts: [
51+
{ isMut: false, isSigner: false, name: 'mint' },
52+
{ isMut: false, isSigner: true, name: 'authority' },
53+
],
54+
name: 'tokenProgram',
55+
},
56+
{
57+
accounts: [
58+
{ isMut: true, isSigner: false, name: 'mint' },
59+
{ isMut: true, isSigner: false, name: 'metadata' },
60+
],
61+
name: 'nftProgram',
62+
},
63+
]);
64+
65+
expect(nodes).toEqual([
66+
instructionAccountNode({ isSigner: false, isWritable: false, name: 'tokenProgramMint' }),
67+
instructionAccountNode({ isSigner: true, isWritable: false, name: 'tokenProgramAuthority' }),
68+
instructionAccountNode({ isSigner: false, isWritable: true, name: 'nftProgramMint' }),
69+
instructionAccountNode({ isSigner: false, isWritable: true, name: 'nftProgramMetadata' }),
70+
]);
71+
});

0 commit comments

Comments
 (0)