@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.
- package/.eslintrc.cjs +29 -0
- package/.prettierignore +3 -0
- package/.prettierrc +6 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/demo/.env.example +8 -0
- package/demo/README.md +125 -0
- package/demo/package.json +26 -0
- package/demo/src/client.ts +54 -0
- package/demo/src/deposit.ts +39 -0
- package/demo/src/server.ts +74 -0
- package/demo/tsconfig.json +8 -0
- package/demo/yarn.lock +925 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/scheme.d.ts +11 -0
- package/dist/client/scheme.js +65 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/server/express/adapter.d.ts +71 -0
- package/dist/server/express/adapter.js +90 -0
- package/dist/server/express/index.d.ts +122 -0
- package/dist/server/express/index.js +340 -0
- package/dist/server/facilitator.d.ts +35 -0
- package/dist/server/facilitator.js +52 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.js +4 -0
- package/dist/server/scheme.d.ts +93 -0
- package/dist/server/scheme.js +179 -0
- package/eslint.config.mjs +22 -0
- package/package.json +79 -0
- package/src/client/index.ts +1 -0
- package/src/client/scheme.ts +95 -0
- package/src/index.ts +7 -0
- package/src/server/express/adapter.ts +100 -0
- package/src/server/express/index.ts +466 -0
- package/src/server/facilitator.ts +90 -0
- package/src/server/index.ts +10 -0
- package/src/server/scheme.ts +223 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +17 -0
- 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
|
+
}
|
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
|
+
}
|
package/vitest.config.ts
ADDED