@4mica/x402 0.1.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/demo/README.md CHANGED
@@ -32,7 +32,7 @@ cp .env.example .env
32
32
 
33
33
  Required variables:
34
34
  - `PRIVATE_KEY`: Your Ethereum private key (with 0x prefix) for Sepolia testnet
35
- - `PAY_TO_ADDRESS`: Address that will receive payments (optional, defaults to a test address)
35
+ - `PAY_TO_ADDRESS`: Address that will receive payments
36
36
 
37
37
  ## Running the Demo
38
38
 
@@ -54,7 +54,7 @@ You should see:
54
54
  ```
55
55
  x402 Demo Server running on http://localhost:3000
56
56
  Protected endpoint: http://localhost:3000/api/premium-data
57
- Payment required: $0.05 (4mica credit on Sepolia)
57
+ Payment required: $0.01 (4mica credit on Sepolia)
58
58
  ```
59
59
 
60
60
  ### Terminal 2: Run the client
@@ -82,7 +82,7 @@ PRIVATE_KEY=0xYourPrivateKey yarn client
82
82
  ## What Happens
83
83
 
84
84
  1. **Server** starts with one protected endpoint: `GET /api/premium-data`
85
- - Requires a payment of $0.05 in 4mica credits on Sepolia
85
+ - Requires a payment of $0.01 in 4mica credits on Sepolia
86
86
  - Uses x402 payment protocol
87
87
 
88
88
  2. **Client** makes a request to the protected endpoint:
package/demo/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "dev": "tsx watch src/server.ts"
11
11
  },
12
12
  "dependencies": {
13
- "@4mica/sdk": "file:../../../../../ts-sdk-4mica",
13
+ "@4mica/sdk": "^0.5.3",
14
14
  "@4mica/x402": "file:..",
15
15
  "@x402/fetch": "^2.2.0",
16
16
  "dotenv": "^16.4.7",
@@ -2,8 +2,6 @@ import 'dotenv/config'
2
2
  import { privateKeyToAccount } from 'viem/accounts'
3
3
  import { Client, ConfigBuilder } from '@4mica/sdk'
4
4
 
5
- const USDC_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'
6
-
7
5
  async function main() {
8
6
  const privateKey = process.env.PRIVATE_KEY
9
7
  if (!privateKey || !privateKey.startsWith('0x')) {
@@ -49,9 +49,21 @@ export class FourMicaEvmScheme {
49
49
  };
50
50
  }
51
51
  else if (x402Version === 2) {
52
+ const resourcePayload = paymentRequirements.extra &&
53
+ typeof paymentRequirements.extra === 'object' &&
54
+ 'resource' in paymentRequirements.extra &&
55
+ typeof paymentRequirements.extra.resource === 'object' &&
56
+ paymentRequirements.extra.resource !== null
57
+ ? paymentRequirements.extra.resource
58
+ : {};
59
+ const resource = {
60
+ url: String(resourcePayload.url ?? ''),
61
+ description: String(resourcePayload.description ?? ''),
62
+ mimeType: String(resourcePayload.mimeType ?? ''),
63
+ };
52
64
  const paymentRequired = {
53
65
  x402Version: 2,
54
- resource: { url: '', description: '', mimeType: '' },
66
+ resource,
55
67
  accepts: [paymentRequirements],
56
68
  };
57
69
  const signed = await x402Flow.signPaymentV2(paymentRequired, paymentRequirements, this.signer.address);
@@ -2,11 +2,14 @@ import { x402HTTPResourceServer, x402ResourceServer, } from '@x402/core/server';
2
2
  import { ExpressAdapter } from './adapter.js';
3
3
  import { FourMicaEvmScheme, SUPPORTED_NETWORKS } from '../scheme.js';
4
4
  import { FourMicaFacilitatorClient } from '../facilitator.js';
5
+ function getHTTPServerInternals(httpServer) {
6
+ return httpServer;
7
+ }
5
8
  function registerNetworkServers(httpServer, tabEndpoint) {
6
9
  const schemeServer = new FourMicaEvmScheme(tabEndpoint);
10
+ const server = getHTTPServerInternals(httpServer);
7
11
  SUPPORTED_NETWORKS.forEach((network) => {
8
- ;
9
- httpServer.ResourceServer.register(network, schemeServer);
12
+ server.ResourceServer.register(network, schemeServer);
10
13
  });
11
14
  }
12
15
  function checkIfBazaarNeeded(routes) {
@@ -17,6 +20,13 @@ function checkIfBazaarNeeded(routes) {
17
20
  return !!(routeConfig.extensions && 'bazaar' in routeConfig.extensions);
18
21
  });
19
22
  }
23
+ function isOpenTabHttpError(error) {
24
+ if (typeof error !== 'object' || error === null) {
25
+ return false;
26
+ }
27
+ const candidate = error;
28
+ return typeof candidate.status === 'number' && 'response' in candidate;
29
+ }
20
30
  /**
21
31
  * Express payment middleware for x402 protocol (direct HTTP server instance).
22
32
  *
@@ -57,12 +67,12 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
57
67
  // Dynamically register bazaar extension if routes declare it and not already registered
58
68
  // Skip if pre-registered (e.g., in serverless environments where static imports are used)
59
69
  let bazaarPromise = null;
60
- if (checkIfBazaarNeeded(httpServer.routesConfig) &&
61
- !httpServer.ResourceServer.hasExtension('bazaar')) {
70
+ const httpServerInternals = getHTTPServerInternals(httpServer);
71
+ if (checkIfBazaarNeeded(httpServerInternals.routesConfig) &&
72
+ !httpServerInternals.ResourceServer.hasExtension('bazaar')) {
62
73
  bazaarPromise = import('@x402/extensions/bazaar')
63
74
  .then(({ bazaarResourceServerExtension }) => {
64
- ;
65
- httpServer.ResourceServer.registerExtension(bazaarResourceServerExtension);
75
+ httpServerInternals.ResourceServer.registerExtension(bazaarResourceServerExtension);
66
76
  })
67
77
  .catch((err) => {
68
78
  console.error('Failed to load bazaar extension:', err);
@@ -82,9 +92,8 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
82
92
  return res.json(openTabResponse);
83
93
  }
84
94
  catch (error) {
85
- if (error instanceof Error && 'status' in error) {
86
- const openTabError = error;
87
- return res.status(openTabError.status).json(openTabError.response);
95
+ if (isOpenTabHttpError(error)) {
96
+ return res.status(error.status).json(error.response);
88
97
  }
89
98
  console.error('Failed to open tab:', error);
90
99
  return res.status(500).json({
@@ -126,7 +135,7 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
126
135
  case 'no-payment-required':
127
136
  // No payment needed, proceed directly to the route handler
128
137
  return next();
129
- case 'payment-error':
138
+ case 'payment-error': {
130
139
  // Payment required but not provided or invalid
131
140
  const { response } = result;
132
141
  res.status(response.status);
@@ -140,7 +149,8 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
140
149
  res.json(response.body || {});
141
150
  }
142
151
  return;
143
- case 'payment-verified':
152
+ }
153
+ case 'payment-verified': {
144
154
  // Payment is valid, need to wrap response for settlement
145
155
  const { paymentPayload, paymentRequirements } = result;
146
156
  // Intercept and buffer all core methods that can commit response to client
@@ -256,6 +266,7 @@ export function paymentMiddlewareFromHTTPServer(httpServer, tabConfig, paywallCo
256
266
  bufferedCalls = [];
257
267
  }
258
268
  return;
269
+ }
259
270
  }
260
271
  };
261
272
  }
@@ -1,5 +1,5 @@
1
1
  import { FacilitatorConfig, HTTPFacilitatorClient } from '@x402/core/server';
2
- import { Network, PaymentRequirements } from '@x402/core/types';
2
+ import { Network, PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/core/types';
3
3
  export interface OpenTabRequest {
4
4
  userAddress: string;
5
5
  recipientAddress: string;
@@ -16,6 +16,16 @@ export interface OpenTabResponse {
16
16
  ttlSeconds: number;
17
17
  nextReqId: string;
18
18
  }
19
+ export interface CertificateResponse {
20
+ claims: string;
21
+ signature: string;
22
+ }
23
+ export type FourMicaSettleResponse = SettleResponse & {
24
+ certificate?: CertificateResponse;
25
+ txHash?: string;
26
+ networkId?: string;
27
+ error?: string;
28
+ };
19
29
  export declare class OpenTabError extends Error {
20
30
  readonly status: number;
21
31
  readonly response: OpenTabResponse;
@@ -24,6 +34,7 @@ export declare class OpenTabError extends Error {
24
34
  export declare class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
25
35
  constructor(config?: FacilitatorConfig);
26
36
  openTab(userAddress: string, paymentRequirements: PaymentRequirements, ttlSeconds?: number): Promise<OpenTabResponse>;
37
+ settle(paymentPayload: PaymentPayload, paymentRequirements: PaymentRequirements): Promise<FourMicaSettleResponse>;
27
38
  /**
28
39
  * Helper to convert objects to JSON-safe format.
29
40
  * Handles BigInt and other non-JSON types.
@@ -39,6 +39,28 @@ export class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
39
39
  }
40
40
  throw new Error(`Facilitator openTab failed (${response.status}): ${JSON.stringify(data)}`);
41
41
  }
42
+ async settle(paymentPayload, paymentRequirements) {
43
+ let headers = {
44
+ 'Content-Type': 'application/json',
45
+ };
46
+ const authHeaders = await this.createAuthHeaders('settle');
47
+ headers = { ...headers, ...authHeaders.headers };
48
+ const response = await fetch(`${this.url}/settle`, {
49
+ method: 'POST',
50
+ headers,
51
+ body: JSON.stringify(this.safeJson({
52
+ x402Version: paymentPayload.x402Version,
53
+ paymentPayload,
54
+ paymentRequirements,
55
+ })),
56
+ });
57
+ const data = (await response.json());
58
+ const normalized = normalizeSettleResponse(data, paymentRequirements);
59
+ if (!response.ok || !normalized.success) {
60
+ throw new Error(`Facilitator settle failed (${response.status}): ${normalized.errorReason ?? normalized.error ?? 'unknown error'}`);
61
+ }
62
+ return normalized;
63
+ }
42
64
  /**
43
65
  * Helper to convert objects to JSON-safe format.
44
66
  * Handles BigInt and other non-JSON types.
@@ -50,3 +72,55 @@ export class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
50
72
  return JSON.parse(JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? value.toString() : value)));
51
73
  }
52
74
  }
75
+ function normalizeSettleResponse(payload, requirements) {
76
+ const transaction = String(payload.transaction ??
77
+ payload.transactionHash ??
78
+ payload.txHash ??
79
+ payload.tx_hash ??
80
+ payload.hash ??
81
+ '');
82
+ const network = String(payload.network ?? payload.networkId ?? payload.network_id ?? requirements.network);
83
+ const errorReason = typeof payload.errorReason === 'string'
84
+ ? payload.errorReason
85
+ : typeof payload.error_reason === 'string'
86
+ ? payload.error_reason
87
+ : typeof payload.error === 'string'
88
+ ? payload.error
89
+ : typeof payload.message === 'string'
90
+ ? payload.message
91
+ : undefined;
92
+ const certificate = payload.certificate &&
93
+ typeof payload.certificate === 'object' &&
94
+ typeof payload.certificate.claims === 'string' &&
95
+ typeof payload.certificate.signature === 'string'
96
+ ? {
97
+ claims: payload.certificate.claims,
98
+ signature: payload.certificate.signature,
99
+ }
100
+ : undefined;
101
+ return {
102
+ success: Boolean(payload.success ?? errorReason === undefined),
103
+ errorReason,
104
+ payer: typeof payload.payer === 'string'
105
+ ? payload.payer
106
+ : typeof payload.userAddress === 'string'
107
+ ? payload.userAddress
108
+ : typeof payload.user_address === 'string'
109
+ ? payload.user_address
110
+ : undefined,
111
+ transaction,
112
+ network,
113
+ certificate,
114
+ txHash: typeof payload.txHash === 'string'
115
+ ? payload.txHash
116
+ : typeof payload.tx_hash === 'string'
117
+ ? payload.tx_hash
118
+ : transaction || undefined,
119
+ networkId: typeof payload.networkId === 'string'
120
+ ? payload.networkId
121
+ : typeof payload.network_id === 'string'
122
+ ? payload.network_id
123
+ : network,
124
+ error: typeof payload.error === 'string' ? payload.error : undefined,
125
+ };
126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4mica/x402",
3
- "version": "0.1.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "TypeScript x402 utilities for interacting with the 4Mica payment network",
6
6
  "license": "MIT",
@@ -5,6 +5,7 @@ import {
5
5
  PaymentRequirementsV1,
6
6
  X402Flow,
7
7
  X402PaymentRequired,
8
+ X402ResourceInfo,
8
9
  } from '@4mica/sdk'
9
10
  import { Account } from 'viem/accounts'
10
11
  import { SUPPORTED_NETWORKS } from '../server/scheme.js'
@@ -73,9 +74,23 @@ export class FourMicaEvmScheme implements SchemeNetworkClient {
73
74
  payload: signed.payload as unknown as Record<string, unknown>,
74
75
  }
75
76
  } else if (x402Version === 2) {
77
+ const resourcePayload =
78
+ paymentRequirements.extra &&
79
+ typeof paymentRequirements.extra === 'object' &&
80
+ 'resource' in paymentRequirements.extra &&
81
+ typeof paymentRequirements.extra.resource === 'object' &&
82
+ paymentRequirements.extra.resource !== null
83
+ ? (paymentRequirements.extra.resource as Record<string, unknown>)
84
+ : {}
85
+
86
+ const resource: X402ResourceInfo = {
87
+ url: String(resourcePayload.url ?? ''),
88
+ description: String(resourcePayload.description ?? ''),
89
+ mimeType: String(resourcePayload.mimeType ?? ''),
90
+ }
76
91
  const paymentRequired: X402PaymentRequired = {
77
92
  x402Version: 2,
78
- resource: { url: '', description: '', mimeType: '' },
93
+ resource,
79
94
  accepts: [paymentRequirements],
80
95
  }
81
96
  const signed = await x402Flow.signPaymentV2(
@@ -36,10 +36,26 @@ interface TabConfig {
36
36
  ttlSeconds?: number
37
37
  }
38
38
 
39
+ interface ResourceServerInternals {
40
+ register: (network: Network, server: SchemeNetworkServer) => unknown
41
+ hasExtension: (extension: string) => boolean
42
+ registerExtension: (extension: unknown) => unknown
43
+ }
44
+
45
+ interface HTTPServerInternals {
46
+ ResourceServer: ResourceServerInternals
47
+ routesConfig: RoutesConfig
48
+ }
49
+
50
+ function getHTTPServerInternals(httpServer: x402HTTPResourceServer): HTTPServerInternals {
51
+ return httpServer as unknown as HTTPServerInternals
52
+ }
53
+
39
54
  function registerNetworkServers(httpServer: x402HTTPResourceServer, tabEndpoint: string) {
40
55
  const schemeServer = new FourMicaEvmScheme(tabEndpoint)
56
+ const server = getHTTPServerInternals(httpServer)
41
57
  SUPPORTED_NETWORKS.forEach((network) => {
42
- ;(httpServer as any).ResourceServer.register(network, schemeServer)
58
+ server.ResourceServer.register(network, schemeServer)
43
59
  })
44
60
  }
45
61
 
@@ -53,6 +69,20 @@ function checkIfBazaarNeeded(routes: RoutesConfig): boolean {
53
69
  })
54
70
  }
55
71
 
72
+ interface OpenTabHttpError {
73
+ status: number
74
+ response: unknown
75
+ }
76
+
77
+ function isOpenTabHttpError(error: unknown): error is OpenTabHttpError {
78
+ if (typeof error !== 'object' || error === null) {
79
+ return false
80
+ }
81
+
82
+ const candidate = error as { status?: unknown; response?: unknown }
83
+ return typeof candidate.status === 'number' && 'response' in candidate
84
+ }
85
+
56
86
  /**
57
87
  * Configuration for registering a payment scheme with a specific network
58
88
  */
@@ -118,13 +148,14 @@ export function paymentMiddlewareFromHTTPServer(
118
148
  // Dynamically register bazaar extension if routes declare it and not already registered
119
149
  // Skip if pre-registered (e.g., in serverless environments where static imports are used)
120
150
  let bazaarPromise: Promise<void> | null = null
151
+ const httpServerInternals = getHTTPServerInternals(httpServer)
121
152
  if (
122
- checkIfBazaarNeeded((httpServer as any).routesConfig) &&
123
- !(httpServer as any).ResourceServer.hasExtension('bazaar')
153
+ checkIfBazaarNeeded(httpServerInternals.routesConfig) &&
154
+ !httpServerInternals.ResourceServer.hasExtension('bazaar')
124
155
  ) {
125
156
  bazaarPromise = import('@x402/extensions/bazaar')
126
157
  .then(({ bazaarResourceServerExtension }) => {
127
- ;(httpServer as any).ResourceServer.registerExtension(bazaarResourceServerExtension)
158
+ httpServerInternals.ResourceServer.registerExtension(bazaarResourceServerExtension)
128
159
  })
129
160
  .catch((err) => {
130
161
  console.error('Failed to load bazaar extension:', err)
@@ -150,9 +181,8 @@ export function paymentMiddlewareFromHTTPServer(
150
181
  // Return the response
151
182
  return res.json(openTabResponse)
152
183
  } catch (error) {
153
- if (error instanceof Error && 'status' in error) {
154
- const openTabError = error as any
155
- return res.status(openTabError.status).json(openTabError.response)
184
+ if (isOpenTabHttpError(error)) {
185
+ return res.status(error.status).json(error.response)
156
186
  }
157
187
  console.error('Failed to open tab:', error)
158
188
  return res.status(500).json({
@@ -200,7 +230,7 @@ export function paymentMiddlewareFromHTTPServer(
200
230
  // No payment needed, proceed directly to the route handler
201
231
  return next()
202
232
 
203
- case 'payment-error':
233
+ case 'payment-error': {
204
234
  // Payment required but not provided or invalid
205
235
  const { response } = result
206
236
  res.status(response.status)
@@ -213,8 +243,9 @@ export function paymentMiddlewareFromHTTPServer(
213
243
  res.json(response.body || {})
214
244
  }
215
245
  return
246
+ }
216
247
 
217
- case 'payment-verified':
248
+ case 'payment-verified': {
218
249
  // Payment is valid, need to wrap response for settlement
219
250
  const { paymentPayload, paymentRequirements } = result
220
251
 
@@ -346,6 +377,7 @@ export function paymentMiddlewareFromHTTPServer(
346
377
  bufferedCalls = []
347
378
  }
348
379
  return
380
+ }
349
381
  }
350
382
  }
351
383
  }
@@ -1,5 +1,5 @@
1
1
  import { FacilitatorConfig, HTTPFacilitatorClient } from '@x402/core/server'
2
- import { Network, PaymentRequirements } from '@x402/core/types'
2
+ import { Network, PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/core/types'
3
3
 
4
4
  const DEFAULT_FACILITATOR_URL = 'https://x402.4mica.xyz'
5
5
 
@@ -21,6 +21,18 @@ export interface OpenTabResponse {
21
21
  nextReqId: string
22
22
  }
23
23
 
24
+ export interface CertificateResponse {
25
+ claims: string
26
+ signature: string
27
+ }
28
+
29
+ export type FourMicaSettleResponse = SettleResponse & {
30
+ certificate?: CertificateResponse
31
+ txHash?: string
32
+ networkId?: string
33
+ error?: string
34
+ }
35
+
24
36
  export class OpenTabError extends Error {
25
37
  constructor(
26
38
  public readonly status: number,
@@ -75,6 +87,41 @@ export class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
75
87
  throw new Error(`Facilitator openTab failed (${response.status}): ${JSON.stringify(data)}`)
76
88
  }
77
89
 
90
+ async settle(
91
+ paymentPayload: PaymentPayload,
92
+ paymentRequirements: PaymentRequirements
93
+ ): Promise<FourMicaSettleResponse> {
94
+ let headers: Record<string, string> = {
95
+ 'Content-Type': 'application/json',
96
+ }
97
+
98
+ const authHeaders = await this.createAuthHeaders('settle')
99
+ headers = { ...headers, ...authHeaders.headers }
100
+
101
+ const response = await fetch(`${this.url}/settle`, {
102
+ method: 'POST',
103
+ headers,
104
+ body: JSON.stringify(
105
+ this.safeJson({
106
+ x402Version: paymentPayload.x402Version,
107
+ paymentPayload,
108
+ paymentRequirements,
109
+ })
110
+ ),
111
+ })
112
+
113
+ const data = (await response.json()) as Record<string, unknown>
114
+ const normalized = normalizeSettleResponse(data, paymentRequirements)
115
+
116
+ if (!response.ok || !normalized.success) {
117
+ throw new Error(
118
+ `Facilitator settle failed (${response.status}): ${normalized.errorReason ?? normalized.error ?? 'unknown error'}`
119
+ )
120
+ }
121
+
122
+ return normalized
123
+ }
124
+
78
125
  /**
79
126
  * Helper to convert objects to JSON-safe format.
80
127
  * Handles BigInt and other non-JSON types.
@@ -88,3 +135,70 @@ export class FourMicaFacilitatorClient extends HTTPFacilitatorClient {
88
135
  )
89
136
  }
90
137
  }
138
+
139
+ function normalizeSettleResponse(
140
+ payload: Record<string, unknown>,
141
+ requirements: PaymentRequirements
142
+ ): FourMicaSettleResponse {
143
+ const transaction = String(
144
+ payload.transaction ??
145
+ payload.transactionHash ??
146
+ payload.txHash ??
147
+ payload.tx_hash ??
148
+ payload.hash ??
149
+ ''
150
+ )
151
+ const network = String(
152
+ payload.network ?? payload.networkId ?? payload.network_id ?? requirements.network
153
+ ) as Network
154
+ const errorReason =
155
+ typeof payload.errorReason === 'string'
156
+ ? payload.errorReason
157
+ : typeof payload.error_reason === 'string'
158
+ ? payload.error_reason
159
+ : typeof payload.error === 'string'
160
+ ? payload.error
161
+ : typeof payload.message === 'string'
162
+ ? payload.message
163
+ : undefined
164
+
165
+ const certificate =
166
+ payload.certificate &&
167
+ typeof payload.certificate === 'object' &&
168
+ typeof (payload.certificate as Record<string, unknown>).claims === 'string' &&
169
+ typeof (payload.certificate as Record<string, unknown>).signature === 'string'
170
+ ? {
171
+ claims: (payload.certificate as Record<string, string>).claims,
172
+ signature: (payload.certificate as Record<string, string>).signature,
173
+ }
174
+ : undefined
175
+
176
+ return {
177
+ success: Boolean(payload.success ?? errorReason === undefined),
178
+ errorReason,
179
+ payer:
180
+ typeof payload.payer === 'string'
181
+ ? payload.payer
182
+ : typeof payload.userAddress === 'string'
183
+ ? payload.userAddress
184
+ : typeof payload.user_address === 'string'
185
+ ? payload.user_address
186
+ : undefined,
187
+ transaction,
188
+ network,
189
+ certificate,
190
+ txHash:
191
+ typeof payload.txHash === 'string'
192
+ ? payload.txHash
193
+ : typeof payload.tx_hash === 'string'
194
+ ? payload.tx_hash
195
+ : transaction || undefined,
196
+ networkId:
197
+ typeof payload.networkId === 'string'
198
+ ? payload.networkId
199
+ : typeof payload.network_id === 'string'
200
+ ? payload.network_id
201
+ : network,
202
+ error: typeof payload.error === 'string' ? payload.error : undefined,
203
+ }
204
+ }
@@ -0,0 +1,97 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { privateKeyToAccount } from 'viem/accounts'
3
+
4
+ import { FourMicaEvmScheme } from '../src/client/scheme.js'
5
+
6
+ describe('FourMicaEvmScheme', () => {
7
+ afterEach(() => {
8
+ vi.restoreAllMocks()
9
+ })
10
+
11
+ it('passes V2 resource metadata through to the SDK flow', async () => {
12
+ const signPaymentV2 = vi.fn().mockResolvedValue({
13
+ payload: {
14
+ claims: {
15
+ version: 'v2',
16
+ validation_request_hash: '0x' + '11'.repeat(32),
17
+ validation_subject_hash: '0x' + '22'.repeat(32),
18
+ },
19
+ },
20
+ })
21
+
22
+ vi.spyOn(FourMicaEvmScheme as never, 'createX402Flow' as never).mockResolvedValue({
23
+ signPayment: vi.fn(),
24
+ signPaymentV2,
25
+ } as never)
26
+
27
+ const scheme = await FourMicaEvmScheme.create(privateKeyToAccount(`0x${'11'.repeat(32)}`))
28
+
29
+ const requirements = {
30
+ scheme: '4mica-credit',
31
+ network: 'eip155:11155111',
32
+ asset: '0x2222222222222222222222222222222222222222',
33
+ amount: '10',
34
+ payTo: '0x1111111111111111111111111111111111111111',
35
+ extra: {
36
+ rpcUrl: 'https://custom.rpc.example',
37
+ validationRegistryAddress: '0x3333333333333333333333333333333333333333',
38
+ validatorAddress: '0x4444444444444444444444444444444444444444',
39
+ validatorAgentId: '7',
40
+ minValidationScore: 80,
41
+ requiredValidationTag: 'hard-finality',
42
+ resource: {
43
+ url: 'https://api.example.com/premium',
44
+ description: 'Premium dataset',
45
+ mimeType: 'application/json',
46
+ },
47
+ },
48
+ }
49
+
50
+ const result = await scheme.createPaymentPayload(2, requirements as never)
51
+
52
+ expect(result.x402Version).toBe(2)
53
+ expect(signPaymentV2).toHaveBeenCalledTimes(1)
54
+
55
+ const paymentRequired = signPaymentV2.mock.calls[0]?.[0]
56
+ expect(paymentRequired.resource).toEqual({
57
+ url: 'https://api.example.com/premium',
58
+ description: 'Premium dataset',
59
+ mimeType: 'application/json',
60
+ })
61
+
62
+ const accepted = signPaymentV2.mock.calls[0]?.[1]
63
+ expect(accepted).toMatchObject({
64
+ scheme: '4mica-credit',
65
+ network: 'eip155:11155111',
66
+ asset: '0x2222222222222222222222222222222222222222',
67
+ amount: '10',
68
+ payTo: '0x1111111111111111111111111111111111111111',
69
+ })
70
+ expect(accepted.extra).toMatchObject({
71
+ validationRegistryAddress: '0x3333333333333333333333333333333333333333',
72
+ validatorAddress: '0x4444444444444444444444444444444444444444',
73
+ validatorAgentId: '7',
74
+ minValidationScore: 80,
75
+ requiredValidationTag: 'hard-finality',
76
+ })
77
+ })
78
+
79
+ it('rejects unsupported x402 versions', async () => {
80
+ vi.spyOn(FourMicaEvmScheme as never, 'createX402Flow' as never).mockResolvedValue({
81
+ signPayment: vi.fn(),
82
+ signPaymentV2: vi.fn(),
83
+ } as never)
84
+
85
+ const scheme = await FourMicaEvmScheme.create(privateKeyToAccount(`0x${'11'.repeat(32)}`))
86
+
87
+ await expect(
88
+ scheme.createPaymentPayload(3, {
89
+ scheme: '4mica-credit',
90
+ network: 'eip155:11155111',
91
+ asset: '0x2222222222222222222222222222222222222222',
92
+ amount: '10',
93
+ payTo: '0x1111111111111111111111111111111111111111',
94
+ } as never)
95
+ ).rejects.toThrow('Unsupported x402Version: 3')
96
+ })
97
+ })
@@ -0,0 +1,174 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { FourMicaFacilitatorClient } from '../src/server/facilitator.js'
4
+
5
+ describe('FourMicaFacilitatorClient', () => {
6
+ afterEach(() => {
7
+ vi.restoreAllMocks()
8
+ vi.unstubAllGlobals()
9
+ })
10
+
11
+ it('normalizes 4mica settle responses and preserves certificate fields', async () => {
12
+ const fetchMock = vi.fn().mockResolvedValue(
13
+ new Response(
14
+ JSON.stringify({
15
+ success: true,
16
+ txHash: '0xdeadbeef',
17
+ networkId: 'eip155:11155111',
18
+ certificate: {
19
+ claims: '0x' + '11'.repeat(32),
20
+ signature: '0x' + '22'.repeat(96),
21
+ },
22
+ }),
23
+ {
24
+ status: 200,
25
+ headers: { 'content-type': 'application/json' },
26
+ }
27
+ )
28
+ )
29
+
30
+ vi.stubGlobal('fetch', fetchMock)
31
+
32
+ const client = new FourMicaFacilitatorClient({ url: 'https://facilitator.example' })
33
+
34
+ const result = await client.settle(
35
+ {
36
+ x402Version: 2,
37
+ accepted: {
38
+ scheme: '4mica-credit',
39
+ network: 'eip155:11155111',
40
+ asset: '0x2222222222222222222222222222222222222222',
41
+ amount: '10',
42
+ payTo: '0x1111111111111111111111111111111111111111',
43
+ },
44
+ payload: {
45
+ claims: {
46
+ version: 'v2',
47
+ },
48
+ signature: '0x1234',
49
+ scheme: 'eip712',
50
+ },
51
+ } as never,
52
+ {
53
+ scheme: '4mica-credit',
54
+ network: 'eip155:11155111',
55
+ asset: '0x2222222222222222222222222222222222222222',
56
+ amount: '10',
57
+ payTo: '0x1111111111111111111111111111111111111111',
58
+ } as never
59
+ )
60
+
61
+ expect(result.success).toBe(true)
62
+ expect(result.transaction).toBe('0xdeadbeef')
63
+ expect(result.network).toBe('eip155:11155111')
64
+ expect(result.txHash).toBe('0xdeadbeef')
65
+ expect(result.networkId).toBe('eip155:11155111')
66
+ expect(result.certificate).toEqual({
67
+ claims: '0x' + '11'.repeat(32),
68
+ signature: '0x' + '22'.repeat(96),
69
+ })
70
+
71
+ const request = fetchMock.mock.calls[0]?.[1]
72
+ expect(typeof request?.body).toBe('string')
73
+ expect(String(request?.body)).toContain('"x402Version":2')
74
+ expect(String(request?.body)).toContain('"version":"v2"')
75
+ })
76
+
77
+ it('normalizes alias fields when the facilitator omits txHash/networkId', async () => {
78
+ const fetchMock = vi.fn().mockResolvedValue(
79
+ new Response(
80
+ JSON.stringify({
81
+ success: true,
82
+ transactionHash: '0xabc123',
83
+ network: 'eip155:80002',
84
+ user_address: '0x9999999999999999999999999999999999999999',
85
+ }),
86
+ {
87
+ status: 200,
88
+ headers: { 'content-type': 'application/json' },
89
+ }
90
+ )
91
+ )
92
+
93
+ vi.stubGlobal('fetch', fetchMock)
94
+
95
+ const client = new FourMicaFacilitatorClient({ url: 'https://facilitator.example' })
96
+ const result = await client.settle(
97
+ {
98
+ x402Version: 2,
99
+ accepted: {
100
+ scheme: '4mica-credit',
101
+ network: 'eip155:80002',
102
+ asset: '0x2222222222222222222222222222222222222222',
103
+ amount: '10',
104
+ payTo: '0x1111111111111111111111111111111111111111',
105
+ },
106
+ payload: {
107
+ claims: { version: 'v2' },
108
+ signature: '0x1234',
109
+ scheme: 'eip712',
110
+ },
111
+ } as never,
112
+ {
113
+ scheme: '4mica-credit',
114
+ network: 'eip155:80002',
115
+ asset: '0x2222222222222222222222222222222222222222',
116
+ amount: '10',
117
+ payTo: '0x1111111111111111111111111111111111111111',
118
+ } as never
119
+ )
120
+
121
+ expect(result.success).toBe(true)
122
+ expect(result.transaction).toBe('0xabc123')
123
+ expect(result.txHash).toBe('0xabc123')
124
+ expect(result.network).toBe('eip155:80002')
125
+ expect(result.networkId).toBe('eip155:80002')
126
+ expect(result.payer).toBe('0x9999999999999999999999999999999999999999')
127
+ })
128
+
129
+ it('surfaces facilitator errors using normalized reasons', async () => {
130
+ const fetchMock = vi.fn().mockResolvedValue(
131
+ new Response(
132
+ JSON.stringify({
133
+ success: false,
134
+ error_reason: 'unsupported x402Version 2',
135
+ }),
136
+ {
137
+ status: 200,
138
+ headers: { 'content-type': 'application/json' },
139
+ }
140
+ )
141
+ )
142
+
143
+ vi.stubGlobal('fetch', fetchMock)
144
+
145
+ const client = new FourMicaFacilitatorClient({ url: 'https://facilitator.example' })
146
+
147
+ await expect(
148
+ client.settle(
149
+ {
150
+ x402Version: 2,
151
+ accepted: {
152
+ scheme: '4mica-credit',
153
+ network: 'eip155:11155111',
154
+ asset: '0x2222222222222222222222222222222222222222',
155
+ amount: '10',
156
+ payTo: '0x1111111111111111111111111111111111111111',
157
+ },
158
+ payload: {
159
+ claims: { version: 'v2' },
160
+ signature: '0x1234',
161
+ scheme: 'eip712',
162
+ },
163
+ } as never,
164
+ {
165
+ scheme: '4mica-credit',
166
+ network: 'eip155:11155111',
167
+ asset: '0x2222222222222222222222222222222222222222',
168
+ amount: '10',
169
+ payTo: '0x1111111111111111111111111111111111111111',
170
+ } as never
171
+ )
172
+ ).rejects.toThrow('unsupported x402Version 2')
173
+ })
174
+ })