@4mica/x402 0.1.0

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.
Files changed (43) hide show
  1. package/.eslintrc.cjs +29 -0
  2. package/.prettierignore +3 -0
  3. package/.prettierrc +6 -0
  4. package/CHANGELOG.md +8 -0
  5. package/LICENSE +21 -0
  6. package/README.md +389 -0
  7. package/demo/.env.example +8 -0
  8. package/demo/README.md +125 -0
  9. package/demo/package.json +26 -0
  10. package/demo/src/client.ts +54 -0
  11. package/demo/src/deposit.ts +39 -0
  12. package/demo/src/server.ts +74 -0
  13. package/demo/tsconfig.json +8 -0
  14. package/demo/yarn.lock +925 -0
  15. package/dist/client/index.d.ts +1 -0
  16. package/dist/client/index.js +1 -0
  17. package/dist/client/scheme.d.ts +11 -0
  18. package/dist/client/scheme.js +65 -0
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js +1 -0
  21. package/dist/server/express/adapter.d.ts +71 -0
  22. package/dist/server/express/adapter.js +90 -0
  23. package/dist/server/express/index.d.ts +122 -0
  24. package/dist/server/express/index.js +340 -0
  25. package/dist/server/facilitator.d.ts +35 -0
  26. package/dist/server/facilitator.js +52 -0
  27. package/dist/server/index.d.ts +6 -0
  28. package/dist/server/index.js +4 -0
  29. package/dist/server/scheme.d.ts +93 -0
  30. package/dist/server/scheme.js +179 -0
  31. package/eslint.config.mjs +22 -0
  32. package/package.json +79 -0
  33. package/src/client/index.ts +1 -0
  34. package/src/client/scheme.ts +95 -0
  35. package/src/index.ts +7 -0
  36. package/src/server/express/adapter.ts +100 -0
  37. package/src/server/express/index.ts +466 -0
  38. package/src/server/facilitator.ts +90 -0
  39. package/src/server/index.ts +10 -0
  40. package/src/server/scheme.ts +223 -0
  41. package/tsconfig.build.json +5 -0
  42. package/tsconfig.json +17 -0
  43. package/vitest.config.ts +12 -0
@@ -0,0 +1,179 @@
1
+ export const SUPPORTED_NETWORKS = ['eip155:11155111', 'eip155:80002'];
2
+ /**
3
+ * EVM server implementation for the 4mica payment scheme.
4
+ */
5
+ export class FourMicaEvmScheme {
6
+ constructor(advertisedTabEndpoint) {
7
+ this.advertisedTabEndpoint = advertisedTabEndpoint;
8
+ this.scheme = '4mica-credit';
9
+ this.moneyParsers = [];
10
+ }
11
+ /**
12
+ * Register a custom money parser in the parser chain.
13
+ * Multiple parsers can be registered - they will be tried in registration order.
14
+ * Each parser receives a decimal amount (e.g., 1.50 for $1.50).
15
+ * If a parser returns null, the next parser in the chain will be tried.
16
+ * The default parser is always the final fallback.
17
+ *
18
+ * @param parser - Custom function to convert amount to AssetAmount (or null to skip)
19
+ * @returns The server instance for chaining
20
+ *
21
+ * @example
22
+ * evmServer.registerMoneyParser(async (amount, network) => {
23
+ * // Custom conversion logic
24
+ * if (amount > 100) {
25
+ * // Use different token for large amounts
26
+ * return { amount: (amount * 1e18).toString(), asset: "0xCustomToken" };
27
+ * }
28
+ * return null; // Use next parser
29
+ * });
30
+ */
31
+ registerMoneyParser(parser) {
32
+ this.moneyParsers.push(parser);
33
+ return this;
34
+ }
35
+ /**
36
+ * Parses a price into an asset amount.
37
+ * If price is already an AssetAmount, returns it directly.
38
+ * If price is Money (string | number), parses to decimal and tries custom parsers.
39
+ * Falls back to default conversion if all custom parsers return null.
40
+ *
41
+ * @param price - The price to parse
42
+ * @param network - The network to use
43
+ * @returns Promise that resolves to the parsed asset amount
44
+ */
45
+ async parsePrice(price, network) {
46
+ // If already an AssetAmount, return it directly
47
+ if (typeof price === 'object' && price !== null && 'amount' in price) {
48
+ if (!price.asset) {
49
+ throw new Error(`Asset address must be specified for AssetAmount on network ${network}`);
50
+ }
51
+ return {
52
+ amount: price.amount,
53
+ asset: price.asset,
54
+ extra: price.extra || {},
55
+ };
56
+ }
57
+ // Parse Money to decimal number
58
+ const amount = this.parseMoneyToDecimal(price);
59
+ // Try each custom money parser in order
60
+ for (const parser of this.moneyParsers) {
61
+ const result = await parser(amount, network);
62
+ if (result !== null) {
63
+ return result;
64
+ }
65
+ }
66
+ // All custom parsers returned null, use default conversion
67
+ return this.defaultMoneyConversion(amount, network);
68
+ }
69
+ /**
70
+ * Build payment requirements for this scheme/network combination
71
+ *
72
+ * @param paymentRequirements - The base payment requirements
73
+ * @param supportedKind - The supported kind from facilitator (unused)
74
+ * @param supportedKind.x402Version - The x402 version
75
+ * @param supportedKind.scheme - The logical payment scheme
76
+ * @param supportedKind.network - The network identifier in CAIP-2 format
77
+ * @param supportedKind.extra - Optional extra metadata regarding scheme/network implementation details
78
+ * @param extensionKeys - Extension keys supported by the facilitator (unused)
79
+ * @returns Payment requirements ready to be sent to clients
80
+ */
81
+ enhancePaymentRequirements(paymentRequirements, supportedKind, extensionKeys) {
82
+ // Mark unused parameters to satisfy linter
83
+ void supportedKind;
84
+ void extensionKeys;
85
+ if (!paymentRequirements.extra) {
86
+ paymentRequirements.extra = {};
87
+ }
88
+ paymentRequirements.extra.tabEndpoint = this.advertisedTabEndpoint;
89
+ return Promise.resolve(paymentRequirements);
90
+ }
91
+ /**
92
+ * Parse Money (string | number) to a decimal number.
93
+ * Handles formats like "$1.50", "1.50", 1.50, etc.
94
+ *
95
+ * @param money - The money value to parse
96
+ * @returns Decimal number
97
+ */
98
+ parseMoneyToDecimal(money) {
99
+ if (typeof money === 'number') {
100
+ return money;
101
+ }
102
+ // Remove $ sign and whitespace, then parse
103
+ const cleanMoney = money.replace(/^\$/, '').trim();
104
+ const amount = parseFloat(cleanMoney);
105
+ if (isNaN(amount)) {
106
+ throw new Error(`Invalid money format: ${money}`);
107
+ }
108
+ return amount;
109
+ }
110
+ /**
111
+ * Default money conversion implementation.
112
+ * Converts decimal amount to the default stablecoin on the specified network.
113
+ *
114
+ * @param amount - The decimal amount (e.g., 1.50)
115
+ * @param network - The network to use
116
+ * @returns The parsed asset amount in the default stablecoin
117
+ */
118
+ defaultMoneyConversion(amount, network) {
119
+ const assetInfo = this.getDefaultAsset(network);
120
+ const tokenAmount = this.convertToTokenAmount(amount.toString(), assetInfo.decimals);
121
+ return {
122
+ amount: tokenAmount,
123
+ asset: assetInfo.address,
124
+ extra: {
125
+ name: assetInfo.name,
126
+ version: assetInfo.version,
127
+ },
128
+ };
129
+ }
130
+ /**
131
+ * Convert decimal amount to token units (e.g., 0.10 -> 100000 for 6-decimal tokens)
132
+ *
133
+ * @param decimalAmount - The decimal amount to convert
134
+ * @param decimals - The number of decimals for the token
135
+ * @returns The token amount as a string
136
+ */
137
+ convertToTokenAmount(decimalAmount, decimals) {
138
+ const amount = parseFloat(decimalAmount);
139
+ if (isNaN(amount)) {
140
+ throw new Error(`Invalid amount: ${decimalAmount}`);
141
+ }
142
+ // Convert to smallest unit (e.g., for USDC with 6 decimals: 0.10 * 10^6 = 100000)
143
+ const [intPart, decPart = ''] = String(amount).split('.');
144
+ const paddedDec = decPart.padEnd(decimals, '0').slice(0, decimals);
145
+ const tokenAmount = (intPart + paddedDec).replace(/^0+/, '') || '0';
146
+ return tokenAmount;
147
+ }
148
+ /**
149
+ * Get the default asset info for a network (typically USDC)
150
+ *
151
+ * @param network - The network to get asset info for
152
+ * @returns The asset information including address, name, version, and decimals
153
+ */
154
+ getDefaultAsset(network) {
155
+ // Map of network to USDC info including EIP-712 domain parameters
156
+ // Each network has the right to determine its own default stablecoin that can be expressed as a USD string by calling servers
157
+ // NOTE: Currently only EIP-3009 supporting stablecoins can be used with this scheme
158
+ // Generic ERC20 support via EIP-2612/permit2 is planned, but not yet implemented.
159
+ const stablecoins = {
160
+ 'eip155:11155111': {
161
+ address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
162
+ name: 'USDC',
163
+ version: '2',
164
+ decimals: 6,
165
+ }, // Ethereum Sepolia USDC
166
+ 'eip155:80002': {
167
+ address: '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582',
168
+ name: 'USDC',
169
+ version: '2',
170
+ decimals: 6,
171
+ }, // Polygon PoS Amoy USDC
172
+ };
173
+ const assetInfo = stablecoins[network];
174
+ if (!assetInfo) {
175
+ throw new Error(`No default asset configured for network ${network}`);
176
+ }
177
+ return assetInfo;
178
+ }
179
+ }
@@ -0,0 +1,22 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import tseslint from "typescript-eslint";
4
+ import pluginReact from "eslint-plugin-react";
5
+ import { defineConfig } from "eslint/config";
6
+
7
+ export default defineConfig([
8
+ {
9
+ ignores: ["dist/**", "coverage/**", "node_modules/**", "*.cjs"],
10
+ },
11
+ {
12
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
13
+ plugins: { js },
14
+ extends: ["js/recommended"],
15
+ languageOptions: { globals: globals.browser },
16
+ },
17
+ tseslint.configs.recommended,
18
+ pluginReact.configs.flat.recommended,
19
+ {
20
+ settings: { react: { version: "detect" } },
21
+ },
22
+ ]);
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@4mica/x402",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "TypeScript x402 utilities for interacting with the 4Mica payment network",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/4mica-Network/x402-4mica.git"
10
+ },
11
+ "homepage": "https://github.com/4mica-Network/x402-4mica#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/4mica-Network/x402-4mica/issues"
14
+ },
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./server": {
23
+ "types": "./dist/server/index.d.ts",
24
+ "import": "./dist/server/index.js"
25
+ },
26
+ "./server/express": {
27
+ "types": "./dist/server/express/index.d.ts",
28
+ "import": "./dist/server/express/index.js"
29
+ },
30
+ "./client": {
31
+ "types": "./dist/client/index.d.ts",
32
+ "import": "./dist/client/index.js"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.build.json",
37
+ "test": "vitest run",
38
+ "lint": "eslint . --ext .ts",
39
+ "fmt": "prettier --check \"{src,tests}/**/*.{ts,js,json}\"",
40
+ "demo:server": "cd demo && yarn server",
41
+ "demo:client": "cd demo && yarn client",
42
+ "demo:dev": "cd demo && yarn dev"
43
+ },
44
+ "dependencies": {
45
+ "@x402/core": "^2.2.0",
46
+ "@x402/extensions": "^2.2.0",
47
+ "@4mica/sdk": "^0.5.3",
48
+ "viem": "^2.45.1"
49
+ },
50
+ "devDependencies": {
51
+ "@arethetypeswrong/cli": "^0.18.2",
52
+ "@eslint/js": "^9.39.2",
53
+ "@types/express": "^5.0.6",
54
+ "@types/node": "^25.0.7",
55
+ "@typescript-eslint/eslint-plugin": "^8.48.1",
56
+ "@typescript-eslint/parser": "^8.48.1",
57
+ "eslint": "^9.39.2",
58
+ "eslint-config-prettier": "^10.1.8",
59
+ "eslint-plugin-react": "^7.37.5",
60
+ "express": "^5.2.1",
61
+ "globals": "^17.0.0",
62
+ "prettier": "^3.7.4",
63
+ "typescript": "^5.3.3",
64
+ "typescript-eslint": "^8.48.1",
65
+ "vitest": "^4.0.17"
66
+ },
67
+ "peerDependencies": {
68
+ "@x402/paywall": "^2.2.0",
69
+ "express": "^4.0.0 || ^5.0.0"
70
+ },
71
+ "peerDependenciesMeta": {
72
+ "@x402/paywall": {
73
+ "optional": true
74
+ }
75
+ },
76
+ "publishConfig": {
77
+ "access": "public"
78
+ }
79
+ }
@@ -0,0 +1 @@
1
+ export * from './scheme.js'
@@ -0,0 +1,95 @@
1
+ import { SchemeNetworkClient, PaymentRequirements, PaymentPayload, Network } from '@x402/core/types'
2
+ import {
3
+ Client,
4
+ ConfigBuilder,
5
+ PaymentRequirementsV1,
6
+ X402Flow,
7
+ X402PaymentRequired,
8
+ } from '@4mica/sdk'
9
+ import { Account } from 'viem/accounts'
10
+ import { SUPPORTED_NETWORKS } from '../server/scheme.js'
11
+
12
+ const NETWORK_RPC_URLS: Record<Network, string> = {
13
+ 'eip155:11155111': 'https://ethereum.sepolia.api.4mica.xyz',
14
+ 'eip155:80002': 'https://api.4mica.xyz',
15
+ }
16
+
17
+ export class FourMicaEvmScheme implements SchemeNetworkClient {
18
+ readonly scheme = '4mica-credit'
19
+
20
+ private constructor(
21
+ private readonly signer: Account,
22
+ // rpcUrl -> x402Flow
23
+ private readonly x402Flows: Map<string, X402Flow>
24
+ ) {}
25
+
26
+ private static async createX402Flow(signer: Account, rpcUrl: string): Promise<X402Flow> {
27
+ const cfg = new ConfigBuilder().rpcUrl(rpcUrl).signer(signer).build()
28
+ const client = await Client.new(cfg)
29
+
30
+ return X402Flow.fromClient(client)
31
+ }
32
+
33
+ static async create(signer: Account): Promise<FourMicaEvmScheme> {
34
+ const x402Flows = new Map<string, X402Flow>()
35
+
36
+ for (const network of SUPPORTED_NETWORKS) {
37
+ const rpcUrl = NETWORK_RPC_URLS[network]
38
+ if (!rpcUrl) continue
39
+
40
+ x402Flows.set(rpcUrl, await FourMicaEvmScheme.createX402Flow(signer, rpcUrl))
41
+ }
42
+
43
+ return new FourMicaEvmScheme(signer, x402Flows)
44
+ }
45
+
46
+ async createPaymentPayload(
47
+ x402Version: number,
48
+ paymentRequirements: PaymentRequirements
49
+ ): Promise<Pick<PaymentPayload, 'x402Version' | 'payload'>> {
50
+ const network = paymentRequirements.network as Network
51
+ if (!network) {
52
+ throw new Error('Network is required in PaymentRequirements')
53
+ }
54
+
55
+ const rpcUrl = (paymentRequirements.extra?.rpcUrl as string) ?? NETWORK_RPC_URLS[network]
56
+ if (!rpcUrl) {
57
+ throw new Error(`No RPC URL configured for network ${network}`)
58
+ }
59
+
60
+ let x402Flow = this.x402Flows.get(rpcUrl)
61
+ if (!x402Flow) {
62
+ x402Flow = await FourMicaEvmScheme.createX402Flow(this.signer, rpcUrl)
63
+ this.x402Flows.set(rpcUrl, x402Flow)
64
+ }
65
+
66
+ if (x402Version === 1) {
67
+ const signed = await x402Flow.signPayment(
68
+ paymentRequirements as unknown as PaymentRequirementsV1,
69
+ this.signer.address
70
+ )
71
+ return {
72
+ x402Version: 1,
73
+ payload: signed.payload as unknown as Record<string, unknown>,
74
+ }
75
+ } else if (x402Version === 2) {
76
+ const paymentRequired: X402PaymentRequired = {
77
+ x402Version: 2,
78
+ resource: { url: '', description: '', mimeType: '' },
79
+ accepts: [paymentRequirements],
80
+ }
81
+ const signed = await x402Flow.signPaymentV2(
82
+ paymentRequired,
83
+ paymentRequirements,
84
+ this.signer.address
85
+ )
86
+
87
+ return {
88
+ x402Version: 2,
89
+ payload: signed.payload as unknown as Record<string, unknown>,
90
+ }
91
+ }
92
+
93
+ throw new Error(`Unsupported x402Version: ${x402Version}`)
94
+ }
95
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export type {
2
+ PaymentRequired,
3
+ PaymentRequirements,
4
+ PaymentPayload,
5
+ Network,
6
+ SchemeNetworkServer,
7
+ } from '@x402/core/types'
@@ -0,0 +1,100 @@
1
+ import { HTTPAdapter } from '@x402/core/server'
2
+ import { Request } from 'express'
3
+
4
+ /**
5
+ * Express adapter implementation
6
+ */
7
+ export class ExpressAdapter implements HTTPAdapter {
8
+ /**
9
+ * Creates a new ExpressAdapter instance.
10
+ *
11
+ * @param req - The Express request object
12
+ */
13
+ constructor(private req: Request) {}
14
+
15
+ /**
16
+ * Gets a header value from the request.
17
+ *
18
+ * @param name - The header name
19
+ * @returns The header value or undefined
20
+ */
21
+ getHeader(name: string): string | undefined {
22
+ const value = this.req.header(name)
23
+ return Array.isArray(value) ? value[0] : value
24
+ }
25
+
26
+ /**
27
+ * Gets the HTTP method of the request.
28
+ *
29
+ * @returns The HTTP method
30
+ */
31
+ getMethod(): string {
32
+ return this.req.method
33
+ }
34
+
35
+ /**
36
+ * Gets the path of the request.
37
+ *
38
+ * @returns The request path
39
+ */
40
+ getPath(): string {
41
+ return this.req.path
42
+ }
43
+
44
+ /**
45
+ * Gets the full URL of the request.
46
+ *
47
+ * @returns The full request URL
48
+ */
49
+ getUrl(): string {
50
+ return `${this.req.protocol}://${this.req.headers.host}${this.req.originalUrl}`
51
+ }
52
+
53
+ /**
54
+ * Gets the Accept header from the request.
55
+ *
56
+ * @returns The Accept header value or empty string
57
+ */
58
+ getAcceptHeader(): string {
59
+ return this.req.header('Accept') || ''
60
+ }
61
+
62
+ /**
63
+ * Gets the User-Agent header from the request.
64
+ *
65
+ * @returns The User-Agent header value or empty string
66
+ */
67
+ getUserAgent(): string {
68
+ return this.req.header('User-Agent') || ''
69
+ }
70
+
71
+ /**
72
+ * Gets all query parameters from the request URL.
73
+ *
74
+ * @returns Record of query parameter key-value pairs
75
+ */
76
+ getQueryParams(): Record<string, string | string[]> {
77
+ return this.req.query as Record<string, string | string[]>
78
+ }
79
+
80
+ /**
81
+ * Gets a specific query parameter by name.
82
+ *
83
+ * @param name - The query parameter name
84
+ * @returns The query parameter value(s) or undefined
85
+ */
86
+ getQueryParam(name: string): string | string[] | undefined {
87
+ const value = this.req.query[name]
88
+ return value as string | string[] | undefined
89
+ }
90
+
91
+ /**
92
+ * Gets the parsed request body.
93
+ * Requires express.json() or express.urlencoded() middleware.
94
+ *
95
+ * @returns The parsed request body
96
+ */
97
+ getBody(): unknown {
98
+ return this.req.body
99
+ }
100
+ }