Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/add-constant-nodes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@codama/visitors-core': minor
'@codama/nodes-from-anchor': minor
'@codama/node-types': minor
'@codama/nodes': minor
---

Add support for constants with new ConstantNode


3 changes: 2 additions & 1 deletion packages/cli/test/exports/mock-idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"instructions": [],
"definedTypes": [],
"errors": [],
"pdas": []
"pdas": [],
"constants": []
},
"additionalPrograms": []
}
15 changes: 15 additions & 0 deletions packages/node-types/src/ConstantNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { CamelCaseString, Docs } from './shared';
import type { TypeNode } from './typeNodes/TypeNode';
import type { ValueNode } from './valueNodes/ValueNode';

export interface ConstantNode<TType extends TypeNode = TypeNode, TValue extends ValueNode = ValueNode> {
readonly kind: 'constantNode';

// Data.
readonly name: CamelCaseString;
readonly docs?: Docs;

// Children.
readonly type: TType;
readonly value: TValue;
}
2 changes: 2 additions & 0 deletions packages/node-types/src/Node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AccountNode } from './AccountNode';
import type { ConstantNode } from './ConstantNode';
import type { RegisteredContextualValueNode } from './contextualValueNodes/ContextualValueNode';
import type { RegisteredCountNode } from './countNodes/CountNode';
import type { DefinedTypeNode } from './DefinedTypeNode';
Expand All @@ -22,6 +23,7 @@ import type { RegisteredValueNode } from './valueNodes/ValueNode';
export type NodeKind = Node['kind'];
export type Node =
| AccountNode
| ConstantNode
| DefinedTypeNode
| ErrorNode
| InstructionAccountNode
Expand Down
3 changes: 3 additions & 0 deletions packages/node-types/src/ProgramNode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AccountNode } from './AccountNode';
import type { ConstantNode } from './ConstantNode';
import type { DefinedTypeNode } from './DefinedTypeNode';
import type { ErrorNode } from './ErrorNode';
import type { InstructionNode } from './InstructionNode';
Expand All @@ -11,6 +12,7 @@ export interface ProgramNode<
TInstructions extends InstructionNode[] = InstructionNode[],
TDefinedTypes extends DefinedTypeNode[] = DefinedTypeNode[],
TErrors extends ErrorNode[] = ErrorNode[],
TConstants extends ConstantNode[] = ConstantNode[],
> {
readonly kind: 'programNode';

Expand All @@ -27,4 +29,5 @@ export interface ProgramNode<
readonly definedTypes: TDefinedTypes;
readonly pdas: TPdas;
readonly errors: TErrors;
readonly constants: TConstants;
}
1 change: 1 addition & 0 deletions packages/node-types/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './AccountNode';
export * from './ConstantNode';
export * from './DefinedTypeNode';
export * from './ErrorNode';
export * from './InstructionAccountNode';
Expand Down
26 changes: 25 additions & 1 deletion packages/nodes-from-anchor/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
import { bytesValueNode, numberValueNode, stringValueNode, ValueNode } from '@codama/nodes';

export function hex(bytes: number[] | Uint8Array): string {
return (bytes as number[]).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
return Array.from(bytes).reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
}

export function parseConstantValue(valueString: string): ValueNode {
if (valueString.startsWith('[') && valueString.endsWith(']')) {
// It's a byte array
try {
const bytes = JSON.parse(valueString) as number[];
const uint8Array = new Uint8Array(bytes);
return bytesValueNode('base16', hex(uint8Array));
} catch {
// Fallback to string if parsing fails
return stringValueNode(valueString);
}
}

if (/^-?\d+$/.test(valueString)) {
// It's a number
return numberValueNode(Number(valueString));
}

// It's a string
return stringValueNode(valueString);
}
21 changes: 21 additions & 0 deletions packages/nodes-from-anchor/src/v00/ConstantNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ConstantNode, constantNode, numberTypeNode, stringTypeNode } from '@codama/nodes';

import { parseConstantValue } from '../utils';
import { IdlV00Constant } from './idl';
import { typeNodeFromAnchorV00 } from './typeNodes/TypeNode';

export function constantNodeFromAnchorV00(idl: Partial<IdlV00Constant>): ConstantNode {
const name = idl.name ?? '';
const valueString = idl.value ?? '';

// For constants, 'bytes' type represents a raw byte array, not a sized string
// so we use u8 to represent the type of each element
const type =
idl.type === 'bytes'
? numberTypeNode('u8')
: idl.type
? typeNodeFromAnchorV00(idl.type)
: stringTypeNode('utf8');

return constantNode(name, type, parseConstantValue(valueString));
}
2 changes: 2 additions & 0 deletions packages/nodes-from-anchor/src/v00/ProgramNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ProgramNode, programNode, ProgramVersion } from '@codama/nodes';

import { accountNodeFromAnchorV00 } from './AccountNode';
import { constantNodeFromAnchorV00 } from './ConstantNode';
import { definedTypeNodeFromAnchorV00 } from './DefinedTypeNode';
import { errorNodeFromAnchorV00 } from './ErrorNode';
import { IdlV00 } from './idl';
Expand All @@ -16,6 +17,7 @@ export function programNodeFromAnchorV00(idl: IdlV00): ProgramNode {
);
return programNode({
accounts,
constants: (idl?.constants ?? []).map(constantNodeFromAnchorV00),
definedTypes: (idl?.types ?? []).map(definedTypeNodeFromAnchorV00),
errors: (idl?.errors ?? []).map(errorNodeFromAnchorV00),
instructions,
Expand Down
1 change: 1 addition & 0 deletions packages/nodes-from-anchor/src/v00/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './AccountNode';
export * from './ConstantNode';
export * from './DefinedTypeNode';
export * from './ErrorNode';
export * from './InstructionAccountNode';
Expand Down
21 changes: 21 additions & 0 deletions packages/nodes-from-anchor/src/v01/ConstantNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ConstantNode, constantNode, numberTypeNode, stringTypeNode } from '@codama/nodes';

import { parseConstantValue } from '../utils';
import { IdlV01Const } from './idl';
import { typeNodeFromAnchorV01 } from './typeNodes/TypeNode';

export function constantNodeFromAnchorV01(idl: Partial<IdlV01Const>): ConstantNode {
const name = idl.name ?? '';
const valueString = idl.value ?? '';

// For constants, 'bytes' type represents a raw byte array, not a sized string
// so we use u8 to represent the type of each element
const type =
idl.type === 'bytes'
? numberTypeNode('u8')
: idl.type
? typeNodeFromAnchorV01(idl.type, { constArgs: {}, typeArgs: {}, types: {} })
: stringTypeNode('utf8');

return constantNode(name, type, parseConstantValue(valueString));
}
2 changes: 2 additions & 0 deletions packages/nodes-from-anchor/src/v01/ProgramNode.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ProgramNode, programNode, ProgramVersion } from '@codama/nodes';

import { accountNodeFromAnchorV01 } from './AccountNode';
import { constantNodeFromAnchorV01 } from './ConstantNode';
import { definedTypeNodeFromAnchorV01 } from './DefinedTypeNode';
import { errorNodeFromAnchorV01 } from './ErrorNode';
import { IdlV01 } from './idl';
Expand All @@ -19,6 +20,7 @@ export function programNodeFromAnchorV01(idl: IdlV01): ProgramNode {

return programNode({
accounts: accountNodes,
constants: (idl.constants ?? []).map(constantNodeFromAnchorV01),
definedTypes,
errors: errors.map(errorNodeFromAnchorV01),
instructions: instructions.map(instruction => instructionNodeFromAnchorV01(instruction, generics)),
Expand Down
1 change: 1 addition & 0 deletions packages/nodes-from-anchor/src/v01/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './AccountNode';
export * from './ConstantNode';
export * from './DefinedTypeNode';
export * from './ErrorNode';
export * from './idl';
Expand Down
73 changes: 73 additions & 0 deletions packages/nodes-from-anchor/test/v00/ConstantNode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { bytesValueNode, constantNode, numberTypeNode, numberValueNode, stringValueNode } from '@codama/nodes';
import { expect, test } from 'vitest';

import { constantNodeFromAnchorV00, programNodeFromAnchorV00 } from '../../src';

test('it parses constant with number type and value', () => {
const node = constantNodeFromAnchorV00({
name: 'max_size',
type: 'u64',
value: '1000',
});

expect(node).toEqual(constantNode('maxSize', numberTypeNode('u64'), numberValueNode(1000)));
});

test('it parses constant with bytes type and value', () => {
const node = constantNodeFromAnchorV00({
name: 'seed_prefix',
type: 'bytes',
value: '[116, 101, 115, 116]', // "test" in bytes
});

expect(node).toEqual(constantNode('seedPrefix', numberTypeNode('u8'), bytesValueNode('base16', '74657374')));
});

test('it parses constant with string value', () => {
const node = constantNodeFromAnchorV00({
name: 'app_name',
type: { defined: 'String' },
value: 'MyApp',
});

// Type should be parsed, value should be string
expect(node.name).toBe('appName');
expect(node.value).toEqual(stringValueNode('MyApp'));
});

test('it handles malformed JSON in value gracefully', () => {
const node = constantNodeFromAnchorV00({
name: 'invalid_bytes',
type: 'bytes',
value: '[invalid json',
});

// Should fallback to string value
expect(node.value).toEqual(stringValueNode('[invalid json'));
});

test('it parses constants in full program', () => {
const node = programNodeFromAnchorV00({
constants: [
{
name: 'max_items',
type: 'u32',
value: '100',
},
{
name: 'seed_prefix',
type: 'bytes',
value: '[97, 98, 99]', // "abc"
},
],
instructions: [],
name: 'my_program',
version: '1.0.0',
});

expect(node.constants).toHaveLength(2);
expect(node.constants[0]).toEqual(constantNode('maxItems', numberTypeNode('u32'), numberValueNode(100)));
expect(node.constants[1]).toEqual(
constantNode('seedPrefix', numberTypeNode('u8'), bytesValueNode('base16', '616263')),
);
});
83 changes: 83 additions & 0 deletions packages/nodes-from-anchor/test/v01/ConstantNode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { bytesValueNode, constantNode, numberTypeNode, numberValueNode, stringValueNode } from '@codama/nodes';
import { expect, test } from 'vitest';

import { constantNodeFromAnchorV01, programNodeFromAnchorV01 } from '../../src';

test('it parses constant with number type and value', () => {
const node = constantNodeFromAnchorV01({
name: 'max_size',
type: 'u64',
value: '1000',
});

expect(node).toEqual(constantNode('maxSize', numberTypeNode('u64'), numberValueNode(1000)));
});

test('it parses constant with bytes type and value', () => {
const node = constantNodeFromAnchorV01({
name: 'seed_prefix',
type: 'bytes',
value: '[116, 101, 115, 116]', // "test" in bytes
});

expect(node).toEqual(constantNode('seedPrefix', numberTypeNode('u8'), bytesValueNode('base16', '74657374')));
});

test('it parses constant with negative numeric value', () => {
const node = constantNodeFromAnchorV01({
name: 'neg_const',
type: 'i8',
value: '-5',
});

expect(node).toEqual(constantNode('negConst', numberTypeNode('i8'), numberValueNode(-5)));
});

test('it parses constant with string value', () => {
const node = constantNodeFromAnchorV01({
name: 'app_name',
type: { defined: { name: 'String' } },
value: 'MyApp',
});

// Type should be parsed, value should be string
expect(node.name).toBe('appName');
expect(node.value).toEqual(stringValueNode('MyApp'));
});

test('it handles malformed JSON in value gracefully', () => {
const node = constantNodeFromAnchorV01({
name: 'bad_constant',
type: 'bytes',
value: '[invalid json',
});

// Should fallback to string value
expect(node.value).toEqual(stringValueNode('[invalid json'));
});

test('it parses constants in full program', () => {
const node = programNodeFromAnchorV01({
address: '1111',
constants: [
{
name: 'max_items',
type: 'u32',
value: '100',
},
{
name: 'seed_prefix',
type: 'bytes',
value: '[97, 98, 99]', // "abc"
},
],
instructions: [],
metadata: { name: 'my_program', spec: '0.1.0', version: '1.0.0' },
});

expect(node.constants).toHaveLength(2);
expect(node.constants[0]).toEqual(constantNode('maxItems', numberTypeNode('u32'), numberValueNode(100)));
expect(node.constants[1]).toEqual(
constantNode('seedPrefix', numberTypeNode('u8'), bytesValueNode('base16', '616263')),
);
});
Loading