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
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,15 @@ export function Form({
)}

{data && (
<pre className="max-h-60 overflow-auto rounded-md bg-muted text-xs">
{data.type === 'json' ? (
data.type === 'json' ? (
<div className="max-h-60 overflow-auto rounded-md bg-muted text-xs">
<JsonViewer data={data.data as JsonValue} />
) : (
<pre className="max-h-60 overflow-auto rounded-md bg-muted p-3 text-xs">
{JSON.stringify(data.data, null, 2)}
</pre>
)}
</pre>
</div>
) : (
<pre className="max-h-60 overflow-auto rounded-md bg-muted p-3 text-xs">
{JSON.stringify(data.data, null, 2)}
</pre>
)
)}
</CardContent>
);
Expand Down
113 changes: 113 additions & 0 deletions apps/scan/src/lib/x402/schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect } from 'vitest';
import { extractFieldsFromSchema } from './schema';
import { Methods } from '@/types/x402';

describe('extractFieldsFromSchema', () => {
it('should extract string-typed query params', () => {
const inputSchema = {
method: 'GET',
queryParams: {
duration: { type: 'string', description: 'Time window' },
include: { type: 'string' },
},
};

const fields = extractFieldsFromSchema(inputSchema, Methods.GET, 'query');
expect(fields).toHaveLength(2);
expect(fields.find(f => f.name === 'duration')?.type).toBe('string');
expect(fields.find(f => f.name === 'include')?.type).toBe('string');
});

it('should handle numeric example values in query params', () => {
const inputSchema = {
method: 'GET',
queryParams: {
page: 1,
duration: '5m',
},
};

const fields = extractFieldsFromSchema(inputSchema, Methods.GET, 'query');
expect(fields).toHaveLength(2);

const pageField = fields.find(f => f.name === 'page');
expect(pageField).toBeDefined();
expect(pageField?.type).toBe('integer');
expect(pageField?.default).toBe('1');

const durationField = fields.find(f => f.name === 'duration');
expect(durationField).toBeDefined();
expect(durationField?.type).toBe('5m');
});

it('should handle boolean example values in query params', () => {
const inputSchema = {
method: 'GET',
queryParams: {
include_gt_community_data: true,
verbose: false,
},
};

const fields = extractFieldsFromSchema(inputSchema, Methods.GET, 'query');
expect(fields).toHaveLength(2);

const boolField = fields.find(f => f.name === 'include_gt_community_data');
expect(boolField).toBeDefined();
expect(boolField?.type).toBe('boolean');
expect(boolField?.default).toBe('true');
expect(boolField?.enum).toEqual(['true', 'false']);

const verboseField = fields.find(f => f.name === 'verbose');
expect(verboseField?.default).toBe('false');
});

it('should handle float numeric values', () => {
const inputSchema = {
method: 'GET',
queryParams: {
threshold: 0.5,
},
};

const fields = extractFieldsFromSchema(inputSchema, Methods.GET, 'query');
const field = fields.find(f => f.name === 'threshold');
expect(field?.type).toBe('number');
expect(field?.default).toBe('0.5');
});

it('should handle mixed param types like CoinGecko trending pools', () => {
const inputSchema = {
method: 'GET',
queryParams: {
page: 1,
duration: '5m',
include_gt_community_data: true,
include: 'base_token,quote_tokens,dex',
},
};

const fields = extractFieldsFromSchema(inputSchema, Methods.GET, 'query');
expect(fields).toHaveLength(4);
expect(fields.map(f => f.name).sort()).toEqual([
'duration',
'include',
'include_gt_community_data',
'page',
]);
});

it('should preserve non-string defaults in schema objects', () => {
const inputSchema = {
method: 'GET',
queryParams: {
limit: { type: 'integer', default: 10 },
active: { type: 'boolean', default: false },
},
};

const fields = extractFieldsFromSchema(inputSchema, Methods.GET, 'query');
expect(fields.find(f => f.name === 'limit')?.default).toBe('10');
expect(fields.find(f => f.name === 'active')?.default).toBe('false');
});
});
26 changes: 25 additions & 1 deletion apps/scan/src/lib/x402/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,30 @@ function expandFields(
continue;
}

// Handle numeric example values (e.g. queryParams: { page: 1 })
if (typeof raw === 'number') {
fields.push({
name: fullName,
type: Number.isInteger(raw) ? 'integer' : 'number',
required: parentRequired?.includes(name) ?? false,
enum: undefined,
default: String(raw),
} satisfies FieldDefinition);
continue;
}

// Handle boolean example values (e.g. queryParams: { verbose: true })
if (typeof raw === 'boolean') {
fields.push({
name: fullName,
type: 'boolean',
required: parentRequired?.includes(name) ?? false,
enum: ['true', 'false'],
default: String(raw),
} satisfies FieldDefinition);
continue;
}

if (typeof raw !== 'object' || !raw) {
continue;
}
Expand All @@ -124,7 +148,7 @@ function expandFields(
? (field.enum as string[])
: undefined;
const fieldDefault =
typeof field.default === 'string' ? field.default : undefined;
field.default != null ? String(field.default) : undefined;

const isFieldRequired =
typeof field.required === 'boolean'
Expand Down