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;
Comment on lines +13 to +14
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw I've decided, I think it's better to keep the design as-is. 🙌

Wrapping a ConstantValueNode inside feels a bit dirty just for the sake of removing a little duplication.

}
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')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure this is correct? This represents a single u8 number. Should this not just be bytesTypeNode() since the parseConstantValue returns a bytesValueNode('base16', ...) anyway?

I also wonder if we should check the parsed value against the unparsed type to make sure they match together. Wdyt?

: 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')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here.

: 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')));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah see this should actually just use the bytesTypeNode() type here to match bytesValueNode(...).

The BytesTypeNode basically just means, it's a type representing raw-bytes without any size constraints. BytesValueNode helps describing the value for these bytes.

});

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'));
Comment on lines +33 to +35
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the type here?

});

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'));
Comment on lines +45 to +46
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the type here as well? Best to just to an expect(node).toEqual(...) so that we assert on the whole result.

});

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')));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I won't repeat all my comments on this file but the same v00 comments apply here.

});

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