Skip to content
2 changes: 2 additions & 0 deletions app/entities/idl/model/formatters/formatted-idl.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Idl } from '@coral-xyz/anchor';
import type { IdlType } from '@coral-xyz/anchor/dist/cjs/idl';

// renderable idl for ui
export type FormattedIdl = {
Expand Down Expand Up @@ -64,6 +65,7 @@ export type ArgField = {
docs: string[];
name: string;
type: string; // type of the field, e.g. "u64", "string", "publicKey", etc.
rawType?: IdlType; // original IDL type for programmatic access
};

export type StructField = {
Expand Down
1 change: 1 addition & 0 deletions app/entities/idl/model/use-format-anchor-idl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export function useFormatAnchorIdl(idl?: Idl): FormattedIdl | null {
args: ix.args.map(arg => ({
docs: arg.docs || [],
name: camelCase(arg.name),
rawType: arg.type,
type: parseIdlType(arg.type),
})),
docs: ix?.docs || [],
Expand Down
2 changes: 1 addition & 1 deletion app/features/idl/formatted-idl/ui/BaseFormattedIdl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function BaseFormattedIdl({
</button>
))}
</div>
<div className={activeTab.id !== 'interact' ? 'e-overflow-x-auto md:e-overflow-x-scroll' : ''}>
<div className={cn('e-mb-0 e-min-h-96', activeTab.id !== 'interact' ? 'table-responsive' : '')}>
<ActiveTab activeTab={activeTab} />
</div>
</div>
Expand Down
312 changes: 311 additions & 1 deletion app/features/idl/interactive-idl/lib/instruction-args.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { IdlType } from '@coral-xyz/anchor/dist/cjs/idl';
import type { ArgField } from '@entities/idl';
import { describe, expect, it } from 'vitest';

import { isRequiredArg } from './instruction-args';
import { getArrayLengthFromIdlType, isArrayArg, isRequiredArg, isVectorArg } from './instruction-args';

describe('isRequiredArg', () => {
describe('required arguments', () => {
Expand Down Expand Up @@ -123,6 +124,24 @@ describe('isRequiredArg', () => {
};
expect(isRequiredArg(arg)).toBe(false);
});

it('should return false for option(vec(u8))', () => {
const arg: ArgField = {
docs: [],
name: 'optionalItems',
type: 'option(vec(u8))',
};
expect(isRequiredArg(arg)).toBe(false);
});

it('should return false for coption(array(u8, 32))', () => {
const arg: ArgField = {
docs: [],
name: 'optionalData',
type: 'coption(array(u8, 32))',
};
expect(isRequiredArg(arg)).toBe(false);
});
});

describe('edge cases', () => {
Expand Down Expand Up @@ -154,3 +173,294 @@ describe('isRequiredArg', () => {
});
});
});

describe('isArrayArg', () => {
describe('array arguments', () => {
it('should return true for simple array types', () => {
const arg: ArgField = {
docs: [],
name: 'data',
type: 'array(u8, 32)',
};
expect(isArrayArg(arg)).toBe(true);
});

it('should return true for array(bool, 3)', () => {
const arg: ArgField = {
docs: [],
name: 'flags',
type: 'array(bool, 3)',
};
expect(isArrayArg(arg)).toBe(true);
});

it('should return true for array(publicKey, 2)', () => {
const arg: ArgField = {
docs: [],
name: 'keys',
type: 'array(publicKey, 2)',
};
expect(isArrayArg(arg)).toBe(true);
});

it('should return true for option(array(bool, 3))', () => {
const arg: ArgField = {
docs: [],
name: 'optionalFlags',
type: 'option(array(bool, 3))',
};
expect(isArrayArg(arg)).toBe(true);
});

it('should return true for coption(array(u8, 32))', () => {
const arg: ArgField = {
docs: [],
name: 'optionalData',
type: 'coption(array(u8, 32))',
};
expect(isArrayArg(arg)).toBe(true);
});
});

describe('non-array arguments', () => {
it('should return false for simple types', () => {
const arg: ArgField = {
docs: [],
name: 'amount',
type: 'u64',
};
expect(isArrayArg(arg)).toBe(false);
});

it('should return false for vector types', () => {
const arg: ArgField = {
docs: [],
name: 'items',
type: 'vec(u8)',
};
expect(isArrayArg(arg)).toBe(false);
});

it('should return false for option types', () => {
const arg: ArgField = {
docs: [],
name: 'value',
type: 'option(u64)',
};
expect(isArrayArg(arg)).toBe(false);
});

it('should return false for types containing "array" but not as array(', () => {
const arg: ArgField = {
docs: [],
name: 'value',
type: 'MyArrayType',
};
expect(isArrayArg(arg)).toBe(false);
});

it('should return false for empty string type', () => {
const arg: ArgField = {
docs: [],
name: 'value',
type: '',
};
expect(isArrayArg(arg)).toBe(false);
});
});
});

describe('isVectorArg', () => {
describe('vector arguments', () => {
it('should return true for simple vector types', () => {
const arg: ArgField = {
docs: [],
name: 'items',
type: 'vec(u8)',
};
expect(isVectorArg(arg)).toBe(true);
});

it('should return true for vec(publicKey)', () => {
const arg: ArgField = {
docs: [],
name: 'keys',
type: 'vec(publicKey)',
};
expect(isVectorArg(arg)).toBe(true);
});

it('should return true for vec(string)', () => {
const arg: ArgField = {
docs: [],
name: 'names',
type: 'vec(string)',
};
expect(isVectorArg(arg)).toBe(true);
});

it('should return true for option(vec(u8))', () => {
const arg: ArgField = {
docs: [],
name: 'optionalItems',
type: 'option(vec(u8))',
};
expect(isVectorArg(arg)).toBe(true);
});

it('should return true for coption(vec(publicKey))', () => {
const arg: ArgField = {
docs: [],
name: 'optionalKeys',
type: 'coption(vec(publicKey))',
};
expect(isVectorArg(arg)).toBe(true);
});

it('should return true for vec(RemainingAccountsSlice)', () => {
const arg: ArgField = {
docs: [],
name: 'accounts',
type: 'vec(RemainingAccountsSlice)',
};
expect(isVectorArg(arg)).toBe(true);
});
});

describe('non-vector arguments', () => {
it('should return false for simple types', () => {
const arg: ArgField = {
docs: [],
name: 'amount',
type: 'u64',
};
expect(isVectorArg(arg)).toBe(false);
});

it('should return false for array types', () => {
const arg: ArgField = {
docs: [],
name: 'data',
type: 'array(u8, 32)',
};
expect(isVectorArg(arg)).toBe(false);
});

it('should return false for option types', () => {
const arg: ArgField = {
docs: [],
name: 'value',
type: 'option(u64)',
};
expect(isVectorArg(arg)).toBe(false);
});

it('should return false for types containing "vec" but not as vec(', () => {
const arg: ArgField = {
docs: [],
name: 'value',
type: 'MyVectorType',
};
expect(isVectorArg(arg)).toBe(false);
});

it('should return false for empty string type', () => {
const arg: ArgField = {
docs: [],
name: 'value',
type: '',
};
expect(isVectorArg(arg)).toBe(false);
});
});
});

describe('getArrayLengthFromIdlType', () => {
describe('array types with length', () => {
it('should return length for simple array types', () => {
const type: IdlType = { array: ['u8', 32] };
expect(getArrayLengthFromIdlType(type)).toBe(32);
});

it('should return length for array of strings', () => {
const type: IdlType = { array: ['string', 2] };
expect(getArrayLengthFromIdlType(type)).toBe(2);
});

it('should return length for array of bools', () => {
const type: IdlType = { array: ['bool', 3] };
expect(getArrayLengthFromIdlType(type)).toBe(3);
});

it('should return length for array of pubkeys', () => {
const type: IdlType = { array: ['pubkey', 5] };
expect(getArrayLengthFromIdlType(type)).toBe(5);
});
});

describe('optional array types', () => {
it('should return length for option(array)', () => {
const type: IdlType = { option: { array: ['u8', 16] } };
expect(getArrayLengthFromIdlType(type)).toBe(16);
});

it('should return length for coption(array)', () => {
const type: IdlType = { coption: { array: ['string', 10] } };
expect(getArrayLengthFromIdlType(type)).toBe(10);
});
});

describe('types without length', () => {
it('should return undefined for vector types', () => {
const type: IdlType = { vec: 'u8' };
expect(getArrayLengthFromIdlType(type)).toBeUndefined();
});

it('should return undefined for array with generic length', () => {
const type: IdlType = { array: ['u8', { generic: 'N' }] };
expect(getArrayLengthFromIdlType(type)).toBeUndefined();
});

it('should return undefined for simple types', () => {
const type: IdlType = 'u64';
expect(getArrayLengthFromIdlType(type)).toBeUndefined();
});

it('should return undefined for option of non-array', () => {
const type: IdlType = { option: 'u64' };
expect(getArrayLengthFromIdlType(type)).toBeUndefined();
});

it('should return undefined for coption of non-array', () => {
const type: IdlType = { coption: 'pubkey' };
expect(getArrayLengthFromIdlType(type)).toBeUndefined();
});

it('should return undefined for option(vec)', () => {
const type: IdlType = { option: { vec: 'u8' } };
expect(getArrayLengthFromIdlType(type)).toBeUndefined();
});

it('should return undefined for coption(vec)', () => {
const type: IdlType = { coption: { vec: 'u8' } };
expect(getArrayLengthFromIdlType(type)).toBeUndefined();
});
});

describe('edge cases', () => {
it('should handle large array lengths', () => {
const type: IdlType = { array: ['u8', 1000] };
expect(getArrayLengthFromIdlType(type)).toBe(1000);
});

it('should handle single element arrays', () => {
const type: IdlType = { array: ['u8', 1] };
expect(getArrayLengthFromIdlType(type)).toBe(1);
});

it('should return undefined for defined types', () => {
const type: IdlType = { defined: { generics: [], name: 'MyStruct' } };
expect(getArrayLengthFromIdlType(type)).toBeUndefined();
});
});
});
31 changes: 31 additions & 0 deletions app/features/idl/interactive-idl/lib/instruction-args.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
import type { IdlType } from '@coral-xyz/anchor/dist/cjs/idl';
import type { ArgField } from '@entities/idl';

/**
* Extract array length from an IdlType.
* Handles option/coption wrapping and returns the numeric length if available.
*/
export function getArrayLengthFromIdlType(type: IdlType): number | undefined {
if (typeof type === 'string') return undefined;

// Unwrap option/coption
let inner: IdlType = type;
if ('option' in type) inner = type.option;
else if ('coption' in type) inner = type.coption;

// Check for array
if (typeof inner === 'object' && 'array' in inner) {
const len = inner.array[1];
// len is IdlArrayLen: number | { generic: string }
return typeof len === 'number' ? len : undefined;
}

return undefined;
}

export function isRequiredArg(arg: ArgField): boolean {
return !/^(option|coption)\(/.test(arg.type);
}

export function isArrayArg(arg: ArgField): boolean {
return /array\(/.test(arg.type);
}

export function isVectorArg(arg: ArgField): boolean {
return /vec\(/.test(arg.type);
}
Loading