Skip to content

Commit 146deb5

Browse files
committed
Adapt nodes-from-anchor to Codama v2
1 parent fcc8af3 commit 146deb5

90 files changed

Lines changed: 1447 additions & 985 deletions

File tree

Some content is hidden

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

‎packages/nodes-from-anchor/src/extractPdasVisitor.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { logWarn } from '@codama/errors';
2-
import { camelCase } from '@codama/fragments/casing';
32
import {
43
assertIsNode,
54
identifierString,
@@ -18,17 +17,18 @@ import { bottomUpTransformerVisitor, getUniqueHashStringVisitor, visit, type Vis
1817

1918
type Fingerprint = string;
2019

20+
/** Hash a PDA regardless of its identifier, so identical PDAs from different accounts are deduplicated. */
2121
function pdaFingerprint(pda: PdaNode, hashVisitor: Visitor<string>): Fingerprint {
22-
return visit(pdaNode({ ...pda, identifier: '' }), hashVisitor);
22+
return visit(pdaNode({ ...pda, identifier: '_' }), hashVisitor);
2323
}
2424

2525
function getUniquePdaName(name: IdentifierString, usedNames: Set<IdentifierString>): IdentifierString {
2626
if (!usedNames.has(name)) return name;
2727
let suffix = 2;
28-
let candidate = identifierString(camelCase(`${name}${suffix}`));
28+
let candidate = identifierString(`${name}${suffix}`);
2929
while (usedNames.has(candidate)) {
3030
suffix++;
31-
candidate = identifierString(camelCase(`${name}${suffix}`));
31+
candidate = identifierString(`${name}${suffix}`);
3232
}
3333
return candidate;
3434
}
@@ -71,7 +71,7 @@ export function extractPdasFromProgram(program: ProgramNode): ProgramNode {
7171
const existingFingerprint = nameToFingerprint.get(resolvedName);
7272

7373
if (existingFingerprint !== undefined && existingFingerprint !== fingerprint) {
74-
resolvedName = identifierString(camelCase(`${instruction.identifier}_${pda.identifier}`));
74+
resolvedName = identifierString(`${instruction.identifier}_${pda.identifier}`);
7575
logWarn(
7676
`PDA name collision: "${pda.identifier}" has different seeds across instructions. ` +
7777
`Renaming to "${resolvedName}".`,

‎packages/nodes-from-anchor/src/utils.ts‎

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import {
22
booleanValueNode,
3+
BytesTypeNode,
4+
bytesTypeNode,
35
bytesValueNode,
6+
fixedSizeTransformNode,
7+
floatValueNode,
8+
integerTypeNode,
9+
integerValueNode,
410
isNode,
5-
numberValueNode,
611
publicKeyValueNode,
12+
sizePrefixTransformNode,
13+
StringTypeNode,
714
stringTypeNode,
815
stringValueNode,
916
TypeNode,
@@ -14,36 +21,79 @@ export function hex(bytes: number[] | Uint8Array): string {
1421
return Array.from(bytes).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
1522
}
1623

24+
/** Join the lines of Anchor docs into a single Markdown string, if any. */
25+
export function docsFromAnchor(docs: string[] | undefined): string | undefined {
26+
return docs && docs.length > 0 ? docs.join('\n') : undefined;
27+
}
28+
29+
/** A fixed-size byte array, e.g. for Anchor's 8-byte discriminators. */
30+
export function fixedSizeBytesTypeNode(size: number): BytesTypeNode {
31+
return bytesTypeNode({ transforms: [fixedSizeTransformNode(size)] });
32+
}
33+
34+
/** A Borsh string or byte array, prefixed by its `u32` size. */
35+
export function borshSizePrefixedTypeNode(type: 'bytes' | 'string'): BytesTypeNode | StringTypeNode {
36+
const transforms = [sizePrefixTransformNode(integerTypeNode('u32'))];
37+
return type === 'string' ? stringTypeNode('utf8', { transforms }) : bytesTypeNode({ transforms });
38+
}
39+
40+
/**
41+
* Remove the Borsh `u32` size prefix of a string or byte array, if any.
42+
*
43+
* Anchor uses unprefixed strings and byte arrays for PDA seeds even though
44+
* the corresponding arguments are Borsh size-prefixed.
45+
*/
46+
export function removeBorshSizePrefix(type: TypeNode): TypeNode {
47+
const isBorshSizePrefixed =
48+
(isNode(type, 'bytesTypeNode') || (isNode(type, 'stringTypeNode') && type.encoding === 'utf8')) &&
49+
type.transforms?.length === 1 &&
50+
isNode(type.transforms[0], 'sizePrefixTransformNode') &&
51+
type.transforms[0].prefix.format === 'u32';
52+
if (!isBorshSizePrefixed) return type;
53+
return isNode(type, 'stringTypeNode') ? stringTypeNode('utf8') : bytesTypeNode();
54+
}
55+
1756
function isByteArray(value: unknown): value is number[] {
1857
return Array.isArray(value) && value.every(n => typeof n === 'number' && Number.isInteger(n) && n >= 0 && n <= 255);
1958
}
2059

60+
/** The canonical decimal representation of a float string, if it can be written without an exponent. */
61+
function canonicalFloat(valueString: string): string | undefined {
62+
if (!/^-?\d+(\.\d+)?$/.test(valueString)) return undefined;
63+
const canonical = String(Number(valueString));
64+
return canonical.includes('e') ? undefined : canonical;
65+
}
66+
2167
export function parseConstantValue(valueString: string, type: TypeNode): { type: TypeNode; value: ValueNode } {
68+
const stringFallback = { type: stringTypeNode('utf8'), value: stringValueNode(valueString) };
69+
2270
if (isNode(type, 'bytesTypeNode')) {
2371
try {
2472
const parsed: unknown = JSON.parse(valueString);
2573
if (isByteArray(parsed)) {
2674
return { type, value: bytesValueNode('base16', hex(new Uint8Array(parsed))) };
2775
}
2876
} catch {
29-
// fall through to the string fallback below
77+
// Not JSON: fall through to the string fallback below.
3078
}
31-
return { type: stringTypeNode('utf8'), value: stringValueNode(valueString) };
79+
return stringFallback;
3280
}
3381

34-
if (isNode(type, 'numberTypeNode')) {
35-
const isFloat = type.format === 'f32' || type.format === 'f64';
36-
const numberRegex = isFloat ? /^-?\d+(\.\d+)?$/ : /^-?\d+$/;
37-
if (numberRegex.test(valueString)) {
38-
return { type, value: numberValueNode(Number(valueString)) };
39-
}
40-
return { type: stringTypeNode('utf8'), value: stringValueNode(valueString) };
82+
if (isNode(type, 'integerTypeNode')) {
83+
// Integers are kept as strings, so 64- and 128-bit values stay lossless.
84+
if (!/^-?\d+$/.test(valueString)) return stringFallback;
85+
return { type, value: integerValueNode(BigInt(valueString).toString()) };
86+
}
87+
88+
if (isNode(type, 'floatTypeNode')) {
89+
const canonical = canonicalFloat(valueString);
90+
return canonical === undefined ? stringFallback : { type, value: floatValueNode(canonical) };
4191
}
4292

4393
if (isNode(type, 'booleanTypeNode')) {
4494
if (valueString === 'true') return { type, value: booleanValueNode(true) };
4595
if (valueString === 'false') return { type, value: booleanValueNode(false) };
46-
return { type: stringTypeNode('utf8'), value: stringValueNode(valueString) };
96+
return stringFallback;
4797
}
4898

4999
if (isNode(type, 'publicKeyTypeNode')) {

‎packages/nodes-from-anchor/src/v00/AccountNode.ts‎

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
1-
import { camelCase } from '@codama/fragments/casing';
21
import {
32
AccountNode,
43
accountNode,
54
assertIsNode,
6-
bytesTypeNode,
75
DiscriminatorNode,
86
fieldDiscriminatorNode,
9-
fixedSizeTypeNode,
107
pdaLinkNode,
118
structFieldTypeNode,
12-
StructTypeNode,
139
structTypeNode,
1410
} from '@codama/nodes';
1511

1612
import { getAnchorAccountDiscriminatorV00 } from '../discriminators';
13+
import { docsFromAnchor, fixedSizeBytesTypeNode } from '../utils';
1714
import { IdlV00AccountDef } from './idl';
1815
import { structTypeNodeFromAnchorV00 } from './typeNodes';
1916

20-
export function accountNodeFromAnchorV00(
21-
idl: IdlV00AccountDef,
22-
origin?: 'anchor' | 'shank',
23-
): AccountNode<StructTypeNode> {
24-
const idlName = idl.name ?? '';
25-
const name = camelCase(idlName);
17+
export function accountNodeFromAnchorV00(idl: IdlV00AccountDef, origin?: 'anchor' | 'shank'): AccountNode {
18+
const name = idl.name ?? '';
2619
const idlStruct = idl.type ?? { fields: [], kind: 'struct' };
2720
let data = structTypeNodeFromAnchorV00(idlStruct);
2821
assertIsNode(data, 'structTypeNode');
@@ -32,10 +25,10 @@ export function accountNodeFromAnchorV00(
3225
let discriminators: DiscriminatorNode[] | undefined;
3326
if (origin === 'anchor') {
3427
const discriminator = structFieldTypeNode({
35-
defaultValue: getAnchorAccountDiscriminatorV00(idlName),
28+
defaultValue: getAnchorAccountDiscriminatorV00(name),
3629
defaultValueStrategy: 'omitted',
3730
identifier: 'discriminator',
38-
type: fixedSizeTypeNode(bytesTypeNode(), 8),
31+
type: fixedSizeBytesTypeNode(8),
3932
});
4033
data = structTypeNode([discriminator, ...(data.fields ?? [])]);
4134
discriminators = [fieldDiscriminatorNode('discriminator')];
@@ -44,7 +37,7 @@ export function accountNodeFromAnchorV00(
4437
return accountNode({
4538
data,
4639
discriminators,
47-
docs: idl.docs ?? [],
40+
docs: docsFromAnchor(idl.docs),
4841
identifier: name,
4942
pda: hasSeeds ? pdaLinkNode(name) : undefined,
5043
size: idl.size,
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { DefinedTypeNode, definedTypeNode } from '@codama/nodes';
22

3+
import { docsFromAnchor } from '../utils';
34
import { IdlV00TypeDef } from './idl';
45
import { typeNodeFromAnchorV00 } from './typeNodes';
56

67
export function definedTypeNodeFromAnchorV00(idl: Partial<IdlV00TypeDef>): DefinedTypeNode {
78
const name = idl.name ?? '';
89
const idlType = idl.type ?? { fields: [], kind: 'struct' };
910
const type = typeNodeFromAnchorV00(idlType);
10-
return definedTypeNode({ docs: idl.docs, identifier: name, type });
11+
return definedTypeNode({ docs: docsFromAnchor(idl.docs), identifier: name, type });
1112
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { ErrorNode, errorNode } from '@codama/nodes';
22

3+
import { docsFromAnchor } from '../utils';
34
import { IdlV00ErrorCode } from './idl';
45

56
export function errorNodeFromAnchorV00(idl: Partial<IdlV00ErrorCode>): ErrorNode {
67
const name = idl.name ?? '';
78
const msg = idl.msg ?? '';
89
return errorNode({
910
code: idl.code ?? -1,
10-
docs: idl.docs ?? [msg ? `${name}: ${msg}` : `${name}`],
11-
message: msg,
11+
docs: docsFromAnchor(idl.docs) ?? (msg ? `${name}: ${msg}` : name),
1212
identifier: name,
13+
message: msg,
1314
});
1415
}

‎packages/nodes-from-anchor/src/v00/EventNode.ts‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
1-
import { camelCase } from '@codama/fragments/casing';
21
import {
3-
bytesTypeNode,
2+
addTypeNodeTransforms,
43
constantDiscriminatorNode,
54
constantValueNode,
65
EventNode,
76
eventNode,
8-
fixedSizeTypeNode,
9-
hiddenPrefixTypeNode,
7+
hiddenPrefixTransformNode,
108
} from '@codama/nodes';
119

1210
import { getAnchorEventDiscriminatorV00 } from '../discriminators';
11+
import { fixedSizeBytesTypeNode } from '../utils';
1312
import { IdlV00Event } from './idl';
1413
import { structTypeNodeFromAnchorV00 } from './typeNodes';
1514

1615
export function eventNodeFromAnchorV00(idl: IdlV00Event): EventNode {
17-
const idlName = idl.name ?? '';
18-
const name = camelCase(idlName);
16+
const name = idl.name ?? '';
1917
const data = structTypeNodeFromAnchorV00({ fields: idl.fields ?? [], kind: 'struct' });
20-
const discriminator = getAnchorEventDiscriminatorV00(idlName);
21-
const discriminatorConstant = constantValueNode(fixedSizeTypeNode(bytesTypeNode(), 8), discriminator);
18+
const discriminatorConstant = constantValueNode(fixedSizeBytesTypeNode(8), getAnchorEventDiscriminatorV00(name));
2219

2320
return eventNode({
24-
data: hiddenPrefixTypeNode(data, [discriminatorConstant]),
21+
data: addTypeNodeTransforms(data, [hiddenPrefixTransformNode([discriminatorConstant])]),
2522
discriminators: [constantDiscriminatorNode(discriminatorConstant)],
2623
identifier: name,
2724
});

‎packages/nodes-from-anchor/src/v00/InstructionAccountNode.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { camelCase } from '@codama/fragments/casing';
22
import { InstructionAccountNode, instructionAccountNode } from '@codama/nodes';
33

4+
import { docsFromAnchor } from '../utils';
45
import { IdlV00Account, IdlV00AccountItem } from './idl';
56

7+
/**
8+
* Whether flattening nested account groups would produce accounts whose
9+
* identifiers collide under the casing-collision rule, in which case
10+
* nested accounts are prefixed by their group identifiers.
11+
*/
612
function hasDuplicateAccountNames(idl: IdlV00AccountItem[]): boolean {
713
const seenNames = new Set<string>();
814

@@ -44,9 +50,8 @@ export function instructionAccountNodesFromAnchorV00(
4450

4551
export function instructionAccountNodeFromAnchorV00(idl: IdlV00Account, prefix?: string): InstructionAccountNode {
4652
const isOptional = idl.optional ?? idl.isOptional ?? false;
47-
const desc = idl.desc ? [idl.desc] : undefined;
4853
return instructionAccountNode({
49-
docs: idl.docs ?? desc ?? [],
54+
docs: docsFromAnchor(idl.docs) ?? (idl.desc || undefined),
5055
identifier: prefix ? `${prefix}_${idl.name ?? ''}` : (idl.name ?? ''),
5156
isOptional,
5257
isSigner: idl.isOptionalSigner ? 'either' : (idl.isSigner ?? false),

‎packages/nodes-from-anchor/src/v00/InstructionArgumentNode.ts‎

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)