Skip to content

Commit 908acba

Browse files
Fully Support Anchor 0.30.0 IDL Seed Spec (#42)
* feat: express anchor account and arg seed definitions in instructions as defaultValue on the instruction account * test(renders-js): e2e test for anchor v01 version of the IDL * Update e2e pnpm-lock files * Fix double type in union * Remove hex utility export * Resolve nested struct type of account data and throw arg error when missing * Add test for account path of length 2 * Simplify types * Ignore PDA default values if some seeds have nested paths --------- Co-authored-by: Loris Leiva <[email protected]>
1 parent 4d0d3af commit 908acba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+11240
-4021
lines changed

.changeset/forty-chefs-know.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@kinobi-so/nodes-from-anchor": minor
3+
"@kinobi-so/errors": minor
4+
"@kinobi-so/nodes": minor
5+
---
6+
7+
set anchor account seed definitions on instructions as defaultValue for the associated instruction account. Removes hoisting PDAs to the program node for the time being.

packages/errors/src/codes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export const KINOBI_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND = 1200011 as const
5252
// Reserve error codes in the range [2100000-2100999].
5353
export const KINOBI_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE = 2100000 as const;
5454
export const KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING = 2100001 as const;
55+
export const KINOBI_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING = 2100002 as const;
56+
export const KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING = 2100003 as const;
57+
export const KINOBI_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED = 2100004 as const;
5558

5659
/**
5760
* A union of every Kinobi error code
@@ -70,6 +73,9 @@ export const KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING = 2100001 as const;
7073
*/
7174
export type KinobiErrorCode =
7275
| typeof KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING
76+
| typeof KINOBI_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING
77+
| typeof KINOBI_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED
78+
| typeof KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING
7379
| typeof KINOBI_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE
7480
| typeof KINOBI_ERROR__LINKED_NODE_NOT_FOUND
7581
| typeof KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE

packages/errors/src/context.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import {
1919
} from '@kinobi-so/node-types';
2020

2121
import {
22+
KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING,
23+
KINOBI_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING,
24+
KINOBI_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED,
25+
KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING,
2226
KINOBI_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE,
2327
KINOBI_ERROR__LINKED_NODE_NOT_FOUND,
2428
KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
@@ -50,6 +54,19 @@ type DefaultUnspecifiedErrorContextToUndefined<T> = {
5054
* - Don't change or remove members of an error's context.
5155
*/
5256
export type KinobiErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
57+
[KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING]: {
58+
name: string;
59+
};
60+
[KINOBI_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING]: {
61+
name: string;
62+
};
63+
[KINOBI_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED]: {
64+
kind: string;
65+
};
66+
[KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING]: {
67+
idlType: string;
68+
path: string;
69+
};
5370
[KINOBI_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE]: {
5471
idlType: string;
5572
};

packages/errors/src/messages.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
import {
77
KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING,
8+
KINOBI_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING,
9+
KINOBI_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED,
10+
KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING,
811
KINOBI_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE,
912
KINOBI_ERROR__LINKED_NODE_NOT_FOUND,
1013
KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
@@ -37,6 +40,9 @@ export const KinobiErrorMessages: Readonly<{
3740
[P in KinobiErrorCode]: string;
3841
}> = {
3942
[KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING]: 'Account type [$name] is missing from the IDL types.',
43+
[KINOBI_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING]: 'Argument name [$name] is missing from the instruction definition.',
44+
[KINOBI_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED]: 'Seed kind [$kind] is not implemented.',
45+
[KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING]: 'Field type is missing for path [$path] in [$idlType].',
4046
[KINOBI_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE]: 'Unrecognized Anchor IDL type [$idlType].',
4147
[KINOBI_ERROR__LINKED_NODE_NOT_FOUND]: 'Could not find linked node [$name] from [$kind].',
4248
[KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE]:
Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
11
# Kinobi ➤ Nodes From Anchor
22

3-
TODO
3+
Parse Anchor IDL version `0.0` or `0.1` (from Anchor `0.30`) into Kinobi node definitions.
4+
5+
```javascript
6+
// node ./kinobi.mjs
7+
8+
import path from 'path';
9+
import { renderRustVisitor, renderJavaScriptVisitor } from '@kinobi-so/renderers';
10+
import { rootNodeFromAnchor } from '@kinobi-so/nodes-from-anchor';
11+
import { readJson } from '@kinobi-so/renderers-core';
12+
import { visit } from '@kinobi-so/visitors-core';
13+
14+
const clientDir = path.join(__dirname, 'clients');
15+
16+
const idlPath = path.join(__dirname, 'target', 'idl', 'anchor_program.json');
17+
const idl = readJson(idlPath);
18+
19+
const node = rootNodeFromAnchor(idl);
20+
21+
const sdkName = idl.metadata.name;
22+
23+
await visit(node, renderJavaScriptVisitor(path.join(clientDir, 'js', sdkName, 'src', 'generated')));
24+
25+
visit(node, renderRustVisitor(path.join(clientDir, 'rust', sdkName, 'src', 'generated'), { format: true }));
26+
```

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { BytesValueNode, bytesValueNode, pascalCase, snakeCase } from '@kinobi-so/nodes';
22
import { sha256 } from '@noble/hashes/sha256';
33

4+
import { hex } from './utils';
5+
46
export const getAnchorDiscriminatorV01 = (discriminator: number[]): BytesValueNode => {
57
return bytesValueNode('base16', hex(new Uint8Array(discriminator)));
68
};
@@ -14,7 +16,3 @@ export const getAnchorAccountDiscriminatorV00 = (idlName: string): BytesValueNod
1416
const hash = sha256(`account:${pascalCase(idlName)}`).slice(0, 8);
1517
return bytesValueNode('base16', hex(hash));
1618
};
17-
18-
function hex(bytes: Uint8Array): string {
19-
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
20-
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function hex(bytes: number[] | Uint8Array): string {
2+
return (bytes as number[]).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
3+
}

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,11 @@ import { IdlV01Account, IdlV01TypeDef } from './idl';
1616
import { typeNodeFromAnchorV01 } from './typeNodes';
1717

1818
export function accountNodeFromAnchorV01(idl: IdlV01Account, types: IdlV01TypeDef[]): AccountNode {
19-
const idlName = idl.name;
20-
const name = camelCase(idlName);
21-
22-
const type = types.find(t => t.name === idl.name);
19+
const name = camelCase(idl.name);
20+
const type = types.find(({ name }) => name === idl.name);
2321

2422
if (!type) {
25-
throw new KinobiError(KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING, {
26-
name,
27-
});
23+
throw new KinobiError(KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING, { name: idl.name });
2824
}
2925

3026
const data = typeNodeFromAnchorV01(type.type);

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

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,129 @@
1-
import { InstructionAccountNode, instructionAccountNode, publicKeyValueNode } from '@kinobi-so/nodes';
1+
import {
2+
KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING,
3+
KINOBI_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING,
4+
KINOBI_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED,
5+
KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING,
6+
KinobiError,
7+
} from '@kinobi-so/errors';
8+
import {
9+
AccountNode,
10+
accountValueNode,
11+
argumentValueNode,
12+
camelCase,
13+
constantPdaSeedNodeFromBytes,
14+
InstructionAccountNode,
15+
instructionAccountNode,
16+
InstructionArgumentNode,
17+
pdaNode,
18+
PdaSeedNode,
19+
PdaSeedValueNode,
20+
pdaSeedValueNode,
21+
PdaValueNode,
22+
pdaValueNode,
23+
publicKeyTypeNode,
24+
PublicKeyValueNode,
25+
publicKeyValueNode,
26+
resolveNestedTypeNode,
27+
variablePdaSeedNode,
28+
} from '@kinobi-so/nodes';
229

3-
import { IdlV01InstructionAccount, IdlV01InstructionAccountItem } from './idl';
30+
import { hex } from '../utils';
31+
import { IdlV01InstructionAccount, IdlV01InstructionAccountItem, IdlV01Seed } from './idl';
432

5-
export function instructionAccountNodesFromAnchorV01(idl: IdlV01InstructionAccountItem[]): InstructionAccountNode[] {
33+
export function instructionAccountNodesFromAnchorV01(
34+
allAccounts: AccountNode[],
35+
instructionArguments: InstructionArgumentNode[],
36+
idl: IdlV01InstructionAccountItem[],
37+
): InstructionAccountNode[] {
638
return idl.flatMap(account =>
739
'accounts' in account
8-
? instructionAccountNodesFromAnchorV01(account.accounts)
9-
: [instructionAccountNodeFromAnchorV01(account)],
40+
? instructionAccountNodesFromAnchorV01(allAccounts, instructionArguments, account.accounts)
41+
: [instructionAccountNodeFromAnchorV01(allAccounts, instructionArguments, account)],
1042
);
1143
}
1244

13-
export function instructionAccountNodeFromAnchorV01(idl: IdlV01InstructionAccount): InstructionAccountNode {
45+
export function instructionAccountNodeFromAnchorV01(
46+
allAccounts: AccountNode[],
47+
instructionArguments: InstructionArgumentNode[],
48+
idl: IdlV01InstructionAccount,
49+
): InstructionAccountNode {
1450
const isOptional = idl.optional ?? false;
1551
const docs = idl.docs ?? [];
1652
const isSigner = idl.signer ?? false;
1753
const isWritable = idl.writable ?? false;
1854
const name = idl.name ?? '';
19-
let defaultValue = undefined;
55+
let defaultValue: PdaValueNode | PublicKeyValueNode | undefined;
2056

2157
if (idl.address) {
2258
defaultValue = publicKeyValueNode(idl.address, name);
59+
} else if (idl.pda) {
60+
// TODO: Handle seeds with nested paths.
61+
// Currently, we gracefully ignore PDA default values if we encounter seeds with nested paths.
62+
const seedsWithNestedPaths = idl.pda.seeds.some(seed => 'path' in seed && seed.path.includes('.'));
63+
if (!seedsWithNestedPaths) {
64+
const [seeds, lookups] = idl.pda.seeds.reduce(
65+
([seeds, lookups], seed: IdlV01Seed) => {
66+
const kind = seed.kind;
67+
68+
switch (kind) {
69+
case 'const':
70+
return [[...seeds, constantPdaSeedNodeFromBytes('base16', hex(seed.value))], lookups];
71+
case 'account': {
72+
const path = seed.path.split('.');
73+
if (path.length === 1) {
74+
return [
75+
[...seeds, variablePdaSeedNode(seed.path, publicKeyTypeNode())],
76+
[...lookups, pdaSeedValueNode(seed.path, accountValueNode(seed.path))],
77+
];
78+
} else if (path.length === 2) {
79+
// TODO: Handle nested account paths.
80+
// Currently, this scenario is never reached.
81+
82+
const accountName = camelCase(seed.account ?? '');
83+
const accountNode = allAccounts.find(({ name }) => name === accountName);
84+
if (!accountNode) {
85+
throw new KinobiError(KINOBI_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING, { kind });
86+
}
87+
88+
const fieldName = camelCase(path[1]);
89+
const accountFields = resolveNestedTypeNode(accountNode.data).fields;
90+
const fieldNode = accountFields.find(({ name }) => name === fieldName);
91+
if (!fieldNode) {
92+
throw new KinobiError(KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING, {
93+
idlType: seed.account,
94+
path: seed.path,
95+
});
96+
}
97+
98+
const seedName = camelCase(seed.path);
99+
return [[...seeds, variablePdaSeedNode(seedName, fieldNode.type)], []];
100+
} else {
101+
throw new KinobiError(KINOBI_ERROR__ANCHOR__TYPE_PATH_MISSING, {
102+
idlType: seed,
103+
path: seed.path,
104+
});
105+
}
106+
}
107+
case 'arg': {
108+
const argumentNode = instructionArguments.find(({ name }) => name === seed.path);
109+
if (!argumentNode) {
110+
throw new KinobiError(KINOBI_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING, { name: seed.path });
111+
}
112+
113+
return [
114+
[...seeds, variablePdaSeedNode(seed.path, argumentNode.type)],
115+
[...lookups, pdaSeedValueNode(seed.path, argumentValueNode(seed.path))],
116+
];
117+
}
118+
default:
119+
throw new KinobiError(KINOBI_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED, { kind });
120+
}
121+
},
122+
<[PdaSeedNode[], PdaSeedValueNode[]]>[[], []],
123+
);
124+
125+
defaultValue = pdaValueNode(pdaNode({ name, seeds }), lookups);
126+
}
23127
}
24128

25129
return instructionAccountNode({

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AccountNode,
23
bytesTypeNode,
34
camelCase,
45
fieldDiscriminatorNode,
@@ -13,7 +14,7 @@ import { IdlV01Instruction } from './idl';
1314
import { instructionAccountNodesFromAnchorV01 } from './InstructionAccountNode';
1415
import { instructionArgumentNodeFromAnchorV01 } from './InstructionArgumentNode';
1516

16-
export function instructionNodeFromAnchorV01(idl: IdlV01Instruction): InstructionNode {
17+
export function instructionNodeFromAnchorV01(allAccounts: AccountNode[], idl: IdlV01Instruction): InstructionNode {
1718
const name = idl.name;
1819
let dataArguments = idl.args.map(instructionArgumentNodeFromAnchorV01);
1920

@@ -27,7 +28,7 @@ export function instructionNodeFromAnchorV01(idl: IdlV01Instruction): Instructio
2728
const discriminators = [fieldDiscriminatorNode('discriminator')];
2829

2930
return instructionNode({
30-
accounts: instructionAccountNodesFromAnchorV01(idl.accounts ?? []),
31+
accounts: instructionAccountNodesFromAnchorV01(allAccounts, dataArguments, idl.accounts ?? []),
3132
arguments: dataArguments,
3233
discriminators,
3334
docs: idl.docs ?? [],

0 commit comments

Comments
 (0)