-
Notifications
You must be signed in to change notification settings - Fork 81
feat(nodes): add ConstantNode support with Anchor parsing #958
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
||
|
|
| 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; | ||
| } | ||
| 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); | ||
| } |
| 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') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure this is correct? This represents a single 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)); | ||
| } | ||
| 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') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
| } | ||
| 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'))); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah see this should actually just use the The |
||
| }); | ||
|
|
||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the type here as well? Best to just to an |
||
| }); | ||
|
|
||
| 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')), | ||
| ); | ||
| }); | ||
| 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'))); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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')), | ||
| ); | ||
| }); | ||
There was a problem hiding this comment.
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
ConstantValueNodeinside feels a bit dirty just for the sake of removing a little duplication.