@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,223 @@
1
+ import {
2
+ AssetAmount,
3
+ Network,
4
+ PaymentRequirements,
5
+ Price,
6
+ SchemeNetworkServer,
7
+ MoneyParser,
8
+ } from '@x402/core/types'
9
+
10
+ export const SUPPORTED_NETWORKS: Network[] = ['eip155:11155111', 'eip155:80002']
11
+
12
+ /**
13
+ * EVM server implementation for the 4mica payment scheme.
14
+ */
15
+ export class FourMicaEvmScheme implements SchemeNetworkServer {
16
+ readonly scheme = '4mica-credit'
17
+ private moneyParsers: MoneyParser[] = []
18
+
19
+ constructor(readonly advertisedTabEndpoint: string) {}
20
+
21
+ /**
22
+ * Register a custom money parser in the parser chain.
23
+ * Multiple parsers can be registered - they will be tried in registration order.
24
+ * Each parser receives a decimal amount (e.g., 1.50 for $1.50).
25
+ * If a parser returns null, the next parser in the chain will be tried.
26
+ * The default parser is always the final fallback.
27
+ *
28
+ * @param parser - Custom function to convert amount to AssetAmount (or null to skip)
29
+ * @returns The server instance for chaining
30
+ *
31
+ * @example
32
+ * evmServer.registerMoneyParser(async (amount, network) => {
33
+ * // Custom conversion logic
34
+ * if (amount > 100) {
35
+ * // Use different token for large amounts
36
+ * return { amount: (amount * 1e18).toString(), asset: "0xCustomToken" };
37
+ * }
38
+ * return null; // Use next parser
39
+ * });
40
+ */
41
+ registerMoneyParser(parser: MoneyParser): FourMicaEvmScheme {
42
+ this.moneyParsers.push(parser)
43
+ return this
44
+ }
45
+
46
+ /**
47
+ * Parses a price into an asset amount.
48
+ * If price is already an AssetAmount, returns it directly.
49
+ * If price is Money (string | number), parses to decimal and tries custom parsers.
50
+ * Falls back to default conversion if all custom parsers return null.
51
+ *
52
+ * @param price - The price to parse
53
+ * @param network - The network to use
54
+ * @returns Promise that resolves to the parsed asset amount
55
+ */
56
+ async parsePrice(price: Price, network: Network): Promise<AssetAmount> {
57
+ // If already an AssetAmount, return it directly
58
+ if (typeof price === 'object' && price !== null && 'amount' in price) {
59
+ if (!price.asset) {
60
+ throw new Error(`Asset address must be specified for AssetAmount on network ${network}`)
61
+ }
62
+ return {
63
+ amount: price.amount,
64
+ asset: price.asset,
65
+ extra: price.extra || {},
66
+ }
67
+ }
68
+
69
+ // Parse Money to decimal number
70
+ const amount = this.parseMoneyToDecimal(price)
71
+
72
+ // Try each custom money parser in order
73
+ for (const parser of this.moneyParsers) {
74
+ const result = await parser(amount, network)
75
+ if (result !== null) {
76
+ return result
77
+ }
78
+ }
79
+
80
+ // All custom parsers returned null, use default conversion
81
+ return this.defaultMoneyConversion(amount, network)
82
+ }
83
+
84
+ /**
85
+ * Build payment requirements for this scheme/network combination
86
+ *
87
+ * @param paymentRequirements - The base payment requirements
88
+ * @param supportedKind - The supported kind from facilitator (unused)
89
+ * @param supportedKind.x402Version - The x402 version
90
+ * @param supportedKind.scheme - The logical payment scheme
91
+ * @param supportedKind.network - The network identifier in CAIP-2 format
92
+ * @param supportedKind.extra - Optional extra metadata regarding scheme/network implementation details
93
+ * @param extensionKeys - Extension keys supported by the facilitator (unused)
94
+ * @returns Payment requirements ready to be sent to clients
95
+ */
96
+ enhancePaymentRequirements(
97
+ paymentRequirements: PaymentRequirements,
98
+ supportedKind: {
99
+ x402Version: number
100
+ scheme: string
101
+ network: Network
102
+ extra?: Record<string, unknown>
103
+ },
104
+ extensionKeys: string[]
105
+ ): Promise<PaymentRequirements> {
106
+ // Mark unused parameters to satisfy linter
107
+ void supportedKind
108
+ void extensionKeys
109
+
110
+ if (!paymentRequirements.extra) {
111
+ paymentRequirements.extra = {}
112
+ }
113
+ paymentRequirements.extra.tabEndpoint = this.advertisedTabEndpoint
114
+
115
+ return Promise.resolve(paymentRequirements)
116
+ }
117
+
118
+ /**
119
+ * Parse Money (string | number) to a decimal number.
120
+ * Handles formats like "$1.50", "1.50", 1.50, etc.
121
+ *
122
+ * @param money - The money value to parse
123
+ * @returns Decimal number
124
+ */
125
+ private parseMoneyToDecimal(money: string | number): number {
126
+ if (typeof money === 'number') {
127
+ return money
128
+ }
129
+
130
+ // Remove $ sign and whitespace, then parse
131
+ const cleanMoney = money.replace(/^\$/, '').trim()
132
+ const amount = parseFloat(cleanMoney)
133
+
134
+ if (isNaN(amount)) {
135
+ throw new Error(`Invalid money format: ${money}`)
136
+ }
137
+
138
+ return amount
139
+ }
140
+
141
+ /**
142
+ * Default money conversion implementation.
143
+ * Converts decimal amount to the default stablecoin on the specified network.
144
+ *
145
+ * @param amount - The decimal amount (e.g., 1.50)
146
+ * @param network - The network to use
147
+ * @returns The parsed asset amount in the default stablecoin
148
+ */
149
+ private defaultMoneyConversion(amount: number, network: Network): AssetAmount {
150
+ const assetInfo = this.getDefaultAsset(network)
151
+ const tokenAmount = this.convertToTokenAmount(amount.toString(), assetInfo.decimals)
152
+
153
+ return {
154
+ amount: tokenAmount,
155
+ asset: assetInfo.address,
156
+ extra: {
157
+ name: assetInfo.name,
158
+ version: assetInfo.version,
159
+ },
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Convert decimal amount to token units (e.g., 0.10 -> 100000 for 6-decimal tokens)
165
+ *
166
+ * @param decimalAmount - The decimal amount to convert
167
+ * @param decimals - The number of decimals for the token
168
+ * @returns The token amount as a string
169
+ */
170
+ private convertToTokenAmount(decimalAmount: string, decimals: number): string {
171
+ const amount = parseFloat(decimalAmount)
172
+ if (isNaN(amount)) {
173
+ throw new Error(`Invalid amount: ${decimalAmount}`)
174
+ }
175
+ // Convert to smallest unit (e.g., for USDC with 6 decimals: 0.10 * 10^6 = 100000)
176
+ const [intPart, decPart = ''] = String(amount).split('.')
177
+ const paddedDec = decPart.padEnd(decimals, '0').slice(0, decimals)
178
+ const tokenAmount = (intPart + paddedDec).replace(/^0+/, '') || '0'
179
+ return tokenAmount
180
+ }
181
+
182
+ /**
183
+ * Get the default asset info for a network (typically USDC)
184
+ *
185
+ * @param network - The network to get asset info for
186
+ * @returns The asset information including address, name, version, and decimals
187
+ */
188
+ private getDefaultAsset(network: Network): {
189
+ address: string
190
+ name: string
191
+ version: string
192
+ decimals: number
193
+ } {
194
+ // Map of network to USDC info including EIP-712 domain parameters
195
+ // Each network has the right to determine its own default stablecoin that can be expressed as a USD string by calling servers
196
+ // NOTE: Currently only EIP-3009 supporting stablecoins can be used with this scheme
197
+ // Generic ERC20 support via EIP-2612/permit2 is planned, but not yet implemented.
198
+ const stablecoins: Record<
199
+ string,
200
+ { address: string; name: string; version: string; decimals: number }
201
+ > = {
202
+ 'eip155:11155111': {
203
+ address: '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238',
204
+ name: 'USDC',
205
+ version: '2',
206
+ decimals: 6,
207
+ }, // Ethereum Sepolia USDC
208
+ 'eip155:80002': {
209
+ address: '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582',
210
+ name: 'USDC',
211
+ version: '2',
212
+ decimals: 6,
213
+ }, // Polygon PoS Amoy USDC
214
+ }
215
+
216
+ const assetInfo = stablecoins[network]
217
+ if (!assetInfo) {
218
+ throw new Error(`No default asset configured for network ${network}`)
219
+ }
220
+
221
+ return assetInfo
222
+ }
223
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["src/**/*"],
4
+ "exclude": ["tests", "dist", "node_modules"]
5
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "Node16",
5
+ "lib": [
6
+ "ES2020"
7
+ ],
8
+ "moduleResolution": "node16",
9
+ "declaration": true,
10
+ "outDir": "dist",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "resolveJsonModule": true,
14
+ "skipLibCheck": true,
15
+ },
16
+ "include": ["src/**/*", "tests/**/*", "vitest.config.ts"]
17
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/**/*.test.ts'],
6
+ exclude: [
7
+ 'dist/**',
8
+ 'node_modules/**',
9
+ ...(process.env.CI ? ['tests/**/*.integration.test.ts'] : []),
10
+ ],
11
+ },
12
+ });