Skip to content

Commit 2720a7f

Browse files
authored
Merge pull request #831 from Merit-Systems/mason/bump-x402-version
bump x402 version
2 parents 81d1a0b + 01e5cca commit 2720a7f

9 files changed

Lines changed: 176 additions & 484 deletions

File tree

apps/scan/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@
138138
"vaul": "^1.1.2",
139139
"viem": "catalog:",
140140
"wagmi": "^2.17.5",
141-
"x402": "catalog:x402-v1",
142141
"zod": "catalog:",
143142
"zod3": "npm:zod@^3.24.2"
144143
},

apps/scan/src/lib/resources.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -122,24 +122,37 @@ export const registerResource = async (
122122
);
123123
}
124124

125-
const mappedAccepts = x402Options
126-
.map(opt => ({
127-
scheme: (opt.scheme ?? 'exact') as 'exact',
128-
network: normalizeChainId(opt.network).replace(
129-
'-',
130-
'_'
131-
) as AcceptsNetwork,
132-
maxAmountRequired:
133-
('amount' in opt ? opt.amount : opt.maxAmountRequired) ?? '0',
134-
payTo: opt.payTo ?? '',
135-
asset: opt.asset,
136-
maxTimeoutSeconds: opt.maxTimeoutSeconds ?? 60,
137-
outputSchema: outputSchemaForDb,
138-
extra: undefined,
139-
}))
140-
.filter(accept =>
141-
(SUPPORTED_CHAINS as readonly string[]).includes(accept.network)
125+
const allMappedAccepts = x402Options.map(opt => ({
126+
scheme: (opt.scheme ?? 'exact') as 'exact',
127+
network: normalizeChainId(opt.network).replace('-', '_') as AcceptsNetwork,
128+
maxAmountRequired:
129+
('amount' in opt ? opt.amount : opt.maxAmountRequired) ?? '0',
130+
payTo: opt.payTo ?? '',
131+
asset: opt.asset,
132+
maxTimeoutSeconds: opt.maxTimeoutSeconds ?? 60,
133+
outputSchema: outputSchemaForDb,
134+
extra: undefined,
135+
}));
136+
137+
const mappedAccepts = allMappedAccepts.filter(accept =>
138+
(SUPPORTED_CHAINS as readonly string[]).includes(accept.network)
139+
);
140+
141+
if (mappedAccepts.length === 0) {
142+
const advertisedNetworks = Array.from(
143+
new Set(allMappedAccepts.map(a => a.network))
142144
);
145+
return {
146+
success: false as const,
147+
data: advisory.paymentRequiredBody,
148+
error: {
149+
type: 'parseResponse' as const,
150+
parseErrors: [
151+
`No supported networks advertised. Got: [${advertisedNetworks.join(', ')}]. Supported: [${(SUPPORTED_CHAINS as readonly string[]).join(', ')}]. Testnets are not indexed.`,
152+
],
153+
},
154+
};
155+
}
143156

144157
const x402Version = x402Options[0]?.version ?? 1;
145158

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Mirrors `ChainIdToNetwork` from the v1 `x402` SDK so the production app
2+
// doesn't depend on a v1 package for what is effectively a static lookup.
3+
// v2 uses CAIP-2 strings natively and ships no equivalent map.
4+
5+
const EvmNetworkToChainId: Record<string, number> = {
6+
abstract: 2741,
7+
'abstract-testnet': 11124,
8+
'base-sepolia': 84532,
9+
base: 8453,
10+
'avalanche-fuji': 43113,
11+
avalanche: 43114,
12+
iotex: 4689,
13+
sei: 1329,
14+
'sei-testnet': 1328,
15+
polygon: 137,
16+
'polygon-amoy': 80002,
17+
peaq: 3338,
18+
story: 1514,
19+
educhain: 41923,
20+
'skale-base-sepolia': 324705682,
21+
};
22+
23+
const SvmNetworkToChainId: Record<string, number> = {
24+
'solana-devnet': 103,
25+
solana: 101,
26+
};
27+
28+
export const ChainIdToNetwork: Record<number, string> = Object.fromEntries(
29+
[
30+
...Object.entries(EvmNetworkToChainId),
31+
...Object.entries(SvmNetworkToChainId),
32+
].map(([network, chainId]) => [chainId, network])
33+
);

apps/scan/src/lib/x402/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { x402ResponseSchemaV2, type X402ResponseV2 } from './v2';
1515
import type { DiscoveryExtension } from '@x402/extensions/bazaar';
1616
import { decodePaymentRequiredHeader } from '@x402/core/http';
17-
import { ChainIdToNetwork } from 'x402/types';
17+
import { ChainIdToNetwork } from './chain-mapping';
1818
import type { ParseResult } from './shared';
1919

2020
export type OutputSchema = OutputSchemaV1;

apps/scan/src/lib/x402/v1/schema.ts

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
import {
2-
ChainIdToNetwork,
3-
HTTPRequestStructureSchema,
4-
PaymentRequirementsSchema,
5-
x402ResponseSchema,
6-
} from 'x402/types';
2+
PaymentRequiredV1Schema,
3+
PaymentRequirementsV1Schema,
4+
} from '@x402/core/schemas';
75
import { z as z3 } from 'zod3';
86

7+
import { ChainIdToNetwork } from '../chain-mapping';
98
import { FieldDefSchema } from '../shared';
109

10+
// Inlined from the v1 `x402` SDK. v2 dropped the typed HTTP request structure
11+
// in favor of an opaque `inputSchema: Record<string, unknown>`, so there's no
12+
// equivalent in `@x402/*` to import.
13+
const HTTPRequestStructureSchema = z3.object({
14+
type: z3.literal('http'),
15+
method: z3.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD']),
16+
queryParams: z3.record(z3.string(), z3.string()).optional(),
17+
bodyType: z3
18+
.enum(['json', 'form-data', 'multipart-form-data', 'text', 'binary'])
19+
.optional(),
20+
bodyFields: z3.record(z3.string(), z3.any()).optional(),
21+
headerFields: z3.record(z3.string(), z3.any()).optional(),
22+
});
23+
1124
export const outputSchemaV1 = z3.object({
1225
input: HTTPRequestStructureSchema.omit({
1326
queryParams: true,
@@ -45,21 +58,19 @@ const networkSchemaV1 = z3.union([
4558
.transform(v => ChainIdToNetwork[Number(v.split(':')[1])]),
4659
]);
4760

48-
export const paymentRequirementsSchemaV1 = PaymentRequirementsSchema.extend({
61+
export const paymentRequirementsSchemaV1 = PaymentRequirementsV1Schema.extend({
4962
network: networkSchemaV1,
5063
outputSchema: outputSchemaV1.optional(),
5164
});
5265

53-
export const x402ResponseSchemaV1 = x402ResponseSchema
54-
.omit({
55-
error: true,
56-
accepts: true,
57-
})
58-
.extend({
59-
x402Version: z3.literal(1).default(1),
60-
error: z3.string().nullish(),
61-
accepts: z3.array(paymentRequirementsSchemaV1).optional(),
62-
});
66+
export const x402ResponseSchemaV1 = PaymentRequiredV1Schema.omit({
67+
error: true,
68+
accepts: true,
69+
}).extend({
70+
x402Version: z3.literal(1).default(1),
71+
error: z3.string().nullish(),
72+
accepts: z3.array(paymentRequirementsSchemaV1).optional(),
73+
});
6374

6475
export type X402ResponseV1 = z3.infer<typeof x402ResponseSchemaV1>;
6576
export type OutputSchemaV1 = z3.infer<typeof outputSchemaV1>;

apps/scan/src/services/db/resources/resource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { toPaginatedResponse } from '@/lib/pagination';
77
import { mixedAddressSchema, supportedChainSchema } from '@/lib/schemas';
88

99
import { SUPPORTED_CHAINS } from '@/types/chain';
10-
import { ChainIdToNetwork } from 'x402/types';
10+
import { ChainIdToNetwork } from '@/lib/x402/chain-mapping';
1111

1212
import type { PaginatedQueryParams } from '@/lib/pagination';
1313
import type { AcceptsNetwork, Prisma } from '@x402scan/scan-db';

apps/scan/src/services/db/resources/response.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
import { scanDb } from '@x402scan/scan-db';
22

3+
import type { Prisma } from '@x402scan/scan-db';
34
import type { ParsedX402Response } from '@/lib/x402';
45

6+
const toPrismaJson = (
7+
response: ParsedX402Response
8+
): Prisma.InputJsonValue => {
9+
// Parsed x402 responses are JSON-safe by schema validation, but Prisma's
10+
// structural JSON input type requires nested objects to have index signatures.
11+
return response as unknown as Prisma.InputJsonValue;
12+
};
13+
514
export const upsertResourceResponse = async (
615
resourceId: string,
716
response: ParsedX402Response
817
) => {
18+
const responseJson = toPrismaJson(response);
919
return await scanDb.resourceResponse.upsert({
1020
where: {
1121
resourceId,
1222
},
1323
update: {
1424
resourceId,
15-
response,
25+
response: responseJson,
1626
},
1727
create: {
1828
resourceId,
19-
response,
29+
response: responseJson,
2030
},
2131
});
2232
};

0 commit comments

Comments
 (0)