mppx 0.8.3 → 0.8.5
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/CHANGELOG.md +14 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +2 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/plugins/stripe.d.ts.map +1 -1
- package/dist/cli/plugins/stripe.js +32 -12
- package/dist/cli/plugins/stripe.js.map +1 -1
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +1 -1
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli/validate/challenge.d.ts +14 -0
- package/dist/cli/validate/challenge.d.ts.map +1 -0
- package/dist/cli/validate/challenge.js +211 -0
- package/dist/cli/validate/challenge.js.map +1 -0
- package/dist/cli/validate/discovery.d.ts +10 -0
- package/dist/cli/validate/discovery.d.ts.map +1 -0
- package/dist/cli/validate/discovery.js +128 -0
- package/dist/cli/validate/discovery.js.map +1 -0
- package/dist/cli/validate/helpers.d.ts +33 -0
- package/dist/cli/validate/helpers.d.ts.map +1 -0
- package/dist/cli/validate/helpers.js +136 -0
- package/dist/cli/validate/helpers.js.map +1 -0
- package/dist/cli/validate/index.d.ts +17 -0
- package/dist/cli/validate/index.d.ts.map +1 -0
- package/dist/cli/validate/index.js +224 -0
- package/dist/cli/validate/index.js.map +1 -0
- package/dist/cli/validate/payment.d.ts +7 -0
- package/dist/cli/validate/payment.d.ts.map +1 -0
- package/dist/cli/validate/payment.js +381 -0
- package/dist/cli/validate/payment.js.map +1 -0
- package/dist/evm/client/Charge.d.ts +2 -2
- package/dist/evm/client/Charge.d.ts.map +1 -1
- package/dist/evm/client/Charge.js +11 -17
- package/dist/evm/client/Charge.js.map +1 -1
- package/dist/evm/server/Charge.d.ts +5 -5
- package/dist/evm/server/Charge.d.ts.map +1 -1
- package/dist/evm/server/Charge.js +15 -7
- package/dist/evm/server/Charge.js.map +1 -1
- package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
- package/dist/tempo/internal/auto-swap.js +6 -1
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/x402/Assets.d.ts +40 -0
- package/dist/x402/Assets.d.ts.map +1 -1
- package/dist/x402/Assets.js +73 -0
- package/dist/x402/Assets.js.map +1 -1
- package/dist/x402/client/Exact.d.ts +2 -2
- package/dist/x402/client/Exact.d.ts.map +1 -1
- package/dist/x402/client/Exact.js +6 -13
- package/dist/x402/client/Exact.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/cli.test.ts +134 -29
- package/src/cli/cli.ts +2 -0
- package/src/cli/mcp.test.ts +1 -0
- package/src/cli/plugins/stripe.ts +31 -12
- package/src/cli/utils.test.ts +36 -2
- package/src/cli/utils.ts +2 -1
- package/src/cli/validate/challenge.ts +335 -0
- package/src/cli/validate/discovery.ts +120 -0
- package/src/cli/validate/helpers.ts +154 -0
- package/src/cli/validate/index.ts +348 -0
- package/src/cli/validate/payment.ts +488 -0
- package/src/cli/validate.test.ts +539 -0
- package/src/evm/PublicInterface.test-d.ts +15 -2
- package/src/evm/client/Charge.test.ts +248 -0
- package/src/evm/client/Charge.ts +12 -23
- package/src/evm/server/Charge.test.ts +140 -0
- package/src/evm/server/Charge.ts +19 -12
- package/src/tempo/internal/auto-swap.test.ts +26 -9
- package/src/tempo/internal/auto-swap.ts +15 -2
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/x402/Assets.test.ts +211 -0
- package/src/x402/Assets.ts +116 -0
- package/src/x402/client/Exact.test.ts +129 -0
- package/src/x402/client/Exact.ts +7 -19
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Challenge } from 'mppx'
|
|
2
2
|
import type { Account } from 'viem'
|
|
3
|
+
import { tokens } from 'viem/tokens'
|
|
3
4
|
import { describe, expect, test, vi } from 'vp/test'
|
|
4
5
|
|
|
5
6
|
import * as Assets from '../Assets.js'
|
|
@@ -96,4 +97,251 @@ describe('evm charge client', () => {
|
|
|
96
97
|
'EVM raw currency allowlists require networks.',
|
|
97
98
|
)
|
|
98
99
|
})
|
|
100
|
+
|
|
101
|
+
test('accepts viem token sets for currency policy and decimals', async () => {
|
|
102
|
+
const signTypedData = vi.fn(async () => '0x1234')
|
|
103
|
+
const client = charge({
|
|
104
|
+
account: {
|
|
105
|
+
...account,
|
|
106
|
+
signTypedData,
|
|
107
|
+
} as unknown as Account,
|
|
108
|
+
authorization: { name: 'USD Coin', version: '2' },
|
|
109
|
+
currencies: tokens.popular,
|
|
110
|
+
maxAmount: '1',
|
|
111
|
+
})
|
|
112
|
+
const challenge = Challenge.from({
|
|
113
|
+
id: 'native',
|
|
114
|
+
intent: 'charge',
|
|
115
|
+
method: 'evm',
|
|
116
|
+
realm: 'api.example.com',
|
|
117
|
+
request: {
|
|
118
|
+
amount: '1000000',
|
|
119
|
+
currency: Assets.baseSepolia.USDC.address,
|
|
120
|
+
methodDetails: {
|
|
121
|
+
chainId: 84532,
|
|
122
|
+
credentialTypes: ['authorization'],
|
|
123
|
+
decimals: 18,
|
|
124
|
+
},
|
|
125
|
+
recipient: '0x2222222222222222222222222222222222222222',
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
await client.createCredential({ challenge } as never)
|
|
130
|
+
|
|
131
|
+
expect(signTypedData).toHaveBeenCalledWith(
|
|
132
|
+
expect.objectContaining({
|
|
133
|
+
domain: expect.objectContaining({
|
|
134
|
+
name: 'USD Coin',
|
|
135
|
+
verifyingContract: Assets.baseSepolia.USDC.address,
|
|
136
|
+
version: '2',
|
|
137
|
+
}),
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('uses viem token decimals for native authorization maxAmount policy', async () => {
|
|
143
|
+
const signTypedData = vi.fn(async () => '0x1234')
|
|
144
|
+
const client = charge({
|
|
145
|
+
account: {
|
|
146
|
+
...account,
|
|
147
|
+
signTypedData,
|
|
148
|
+
} as unknown as Account,
|
|
149
|
+
authorization: { name: 'USD Coin', version: '2' },
|
|
150
|
+
currencies: tokens.popular,
|
|
151
|
+
maxAmount: '1',
|
|
152
|
+
})
|
|
153
|
+
const challenge = Challenge.from({
|
|
154
|
+
id: 'native',
|
|
155
|
+
intent: 'charge',
|
|
156
|
+
method: 'evm',
|
|
157
|
+
realm: 'api.example.com',
|
|
158
|
+
request: {
|
|
159
|
+
amount: '1000001',
|
|
160
|
+
currency: Assets.baseSepolia.USDC.address,
|
|
161
|
+
methodDetails: {
|
|
162
|
+
chainId: 84532,
|
|
163
|
+
credentialTypes: ['authorization'],
|
|
164
|
+
decimals: 18,
|
|
165
|
+
},
|
|
166
|
+
recipient: '0x2222222222222222222222222222222222222222',
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
await expect(client.createCredential({ challenge } as never)).rejects.toThrow(
|
|
171
|
+
'EVM charge amount exceeds maxAmount.',
|
|
172
|
+
)
|
|
173
|
+
expect(signTypedData).not.toHaveBeenCalled()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('requires authorization metadata when signing viem token currencies', async () => {
|
|
177
|
+
const signTypedData = vi.fn(async () => '0x1234')
|
|
178
|
+
const client = charge({
|
|
179
|
+
account: {
|
|
180
|
+
...account,
|
|
181
|
+
signTypedData,
|
|
182
|
+
} as unknown as Account,
|
|
183
|
+
currencies: tokens.popular,
|
|
184
|
+
maxAmount: '1',
|
|
185
|
+
})
|
|
186
|
+
const challenge = Challenge.from({
|
|
187
|
+
id: 'native',
|
|
188
|
+
intent: 'charge',
|
|
189
|
+
method: 'evm',
|
|
190
|
+
realm: 'api.example.com',
|
|
191
|
+
request: {
|
|
192
|
+
amount: '1000000',
|
|
193
|
+
currency: Assets.baseSepolia.USDC.address,
|
|
194
|
+
methodDetails: {
|
|
195
|
+
chainId: 84532,
|
|
196
|
+
credentialTypes: ['authorization'],
|
|
197
|
+
decimals: 18,
|
|
198
|
+
},
|
|
199
|
+
recipient: '0x2222222222222222222222222222222222222222',
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
await expect(client.createCredential({ challenge } as never)).rejects.toThrow(
|
|
204
|
+
'EVM authorization requires token name and version.',
|
|
205
|
+
)
|
|
206
|
+
expect(signTypedData).not.toHaveBeenCalled()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('accepts viem token sets through legacy assets policy', async () => {
|
|
210
|
+
const signTypedData = vi.fn(async () => '0x1234')
|
|
211
|
+
const client = charge({
|
|
212
|
+
account: {
|
|
213
|
+
...account,
|
|
214
|
+
signTypedData,
|
|
215
|
+
} as unknown as Account,
|
|
216
|
+
assets: tokens.popular,
|
|
217
|
+
authorization: { name: 'USD Coin', version: '2' },
|
|
218
|
+
maxAmount: '1',
|
|
219
|
+
})
|
|
220
|
+
const challenge = Challenge.from({
|
|
221
|
+
id: 'native',
|
|
222
|
+
intent: 'charge',
|
|
223
|
+
method: 'evm',
|
|
224
|
+
realm: 'api.example.com',
|
|
225
|
+
request: {
|
|
226
|
+
amount: '1000000',
|
|
227
|
+
currency: Assets.baseSepolia.USDC.address,
|
|
228
|
+
methodDetails: {
|
|
229
|
+
chainId: 84532,
|
|
230
|
+
credentialTypes: ['authorization'],
|
|
231
|
+
decimals: 18,
|
|
232
|
+
},
|
|
233
|
+
recipient: '0x2222222222222222222222222222222222222222',
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
await client.createCredential({ challenge } as never)
|
|
238
|
+
|
|
239
|
+
expect(signTypedData).toHaveBeenCalledOnce()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
test('uses known asset metadata for native authorization policy and signing domain', async () => {
|
|
243
|
+
const signTypedData = vi.fn(async () => '0x1234')
|
|
244
|
+
const client = charge({
|
|
245
|
+
account: {
|
|
246
|
+
...account,
|
|
247
|
+
signTypedData,
|
|
248
|
+
} as unknown as Account,
|
|
249
|
+
currencies: [Assets.baseSepolia.USDC],
|
|
250
|
+
maxAmount: '0.01',
|
|
251
|
+
})
|
|
252
|
+
const challenge = Challenge.from({
|
|
253
|
+
id: 'native',
|
|
254
|
+
intent: 'charge',
|
|
255
|
+
method: 'evm',
|
|
256
|
+
realm: 'api.example.com',
|
|
257
|
+
request: {
|
|
258
|
+
amount: '10000',
|
|
259
|
+
currency: Assets.baseSepolia.USDC.address,
|
|
260
|
+
methodDetails: {
|
|
261
|
+
chainId: 84532,
|
|
262
|
+
credentialTypes: ['authorization'],
|
|
263
|
+
decimals: 18,
|
|
264
|
+
},
|
|
265
|
+
recipient: '0x2222222222222222222222222222222222222222',
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
await client.createCredential({ challenge } as never)
|
|
270
|
+
|
|
271
|
+
expect(signTypedData).toHaveBeenCalledOnce()
|
|
272
|
+
expect(signTypedData).toHaveBeenCalledWith(
|
|
273
|
+
expect.objectContaining({
|
|
274
|
+
domain: expect.objectContaining({
|
|
275
|
+
name: 'USDC',
|
|
276
|
+
version: '2',
|
|
277
|
+
}),
|
|
278
|
+
}),
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('uses known asset decimals for native authorization maxAmount policy', async () => {
|
|
283
|
+
const signTypedData = vi.fn(async () => '0x1234')
|
|
284
|
+
const client = charge({
|
|
285
|
+
account: {
|
|
286
|
+
...account,
|
|
287
|
+
signTypedData,
|
|
288
|
+
} as unknown as Account,
|
|
289
|
+
currencies: [Assets.baseSepolia.USDC],
|
|
290
|
+
maxAmount: '0.01',
|
|
291
|
+
})
|
|
292
|
+
const challenge = Challenge.from({
|
|
293
|
+
id: 'native',
|
|
294
|
+
intent: 'charge',
|
|
295
|
+
method: 'evm',
|
|
296
|
+
realm: 'api.example.com',
|
|
297
|
+
request: {
|
|
298
|
+
amount: '10001',
|
|
299
|
+
currency: Assets.baseSepolia.USDC.address,
|
|
300
|
+
methodDetails: {
|
|
301
|
+
chainId: 84532,
|
|
302
|
+
credentialTypes: ['authorization'],
|
|
303
|
+
decimals: 18,
|
|
304
|
+
},
|
|
305
|
+
recipient: '0x2222222222222222222222222222222222222222',
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
await expect(client.createCredential({ challenge } as never)).rejects.toThrow(
|
|
310
|
+
'EVM charge amount exceeds maxAmount.',
|
|
311
|
+
)
|
|
312
|
+
expect(signTypedData).not.toHaveBeenCalled()
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
test('pins known asset native authorization policy to its network', async () => {
|
|
316
|
+
const signTypedData = vi.fn(async () => '0x1234')
|
|
317
|
+
const client = charge({
|
|
318
|
+
account: {
|
|
319
|
+
...account,
|
|
320
|
+
signTypedData,
|
|
321
|
+
} as unknown as Account,
|
|
322
|
+
currencies: [Assets.baseSepolia.USDC],
|
|
323
|
+
maxAmount: '0.01',
|
|
324
|
+
})
|
|
325
|
+
const challenge = Challenge.from({
|
|
326
|
+
id: 'native',
|
|
327
|
+
intent: 'charge',
|
|
328
|
+
method: 'evm',
|
|
329
|
+
realm: 'api.example.com',
|
|
330
|
+
request: {
|
|
331
|
+
amount: '10000',
|
|
332
|
+
currency: Assets.baseSepolia.USDC.address,
|
|
333
|
+
methodDetails: {
|
|
334
|
+
chainId: 8453,
|
|
335
|
+
credentialTypes: ['authorization'],
|
|
336
|
+
decimals: 6,
|
|
337
|
+
},
|
|
338
|
+
recipient: '0x2222222222222222222222222222222222222222',
|
|
339
|
+
},
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
await expect(client.createCredential({ challenge } as never)).rejects.toThrow(
|
|
343
|
+
'EVM currency is not allowed: 0x036CbD53842c5426634e7929541eC2318f3dCF7e.',
|
|
344
|
+
)
|
|
345
|
+
expect(signTypedData).not.toHaveBeenCalled()
|
|
346
|
+
})
|
|
99
347
|
})
|
package/src/evm/client/Charge.ts
CHANGED
|
@@ -106,9 +106,9 @@ export declare namespace charge {
|
|
|
106
106
|
/** Optional allowlist of supported EVM chain IDs. */
|
|
107
107
|
networks?: readonly number[] | undefined
|
|
108
108
|
/** Optional allowlist of supported currencies. */
|
|
109
|
-
currencies?: readonly
|
|
109
|
+
currencies?: readonly Assets.Currency[] | undefined
|
|
110
110
|
/** Legacy alias for `currencies`. */
|
|
111
|
-
assets?: readonly
|
|
111
|
+
assets?: readonly Assets.Currency[] | undefined
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -127,12 +127,12 @@ function assertPolicy(parameters: charge.Parameters, request: Types.ChargeReques
|
|
|
127
127
|
throw new Error(`EVM chain ID is not allowed: ${chainId}.`)
|
|
128
128
|
|
|
129
129
|
const currencies = parameters.currencies ?? parameters.assets
|
|
130
|
-
if (currencies?.some((currency) =>
|
|
130
|
+
if (currencies?.some((currency) => Assets.isRawAddress(currency)) && !parameters.networks?.length)
|
|
131
131
|
throw new Error('EVM raw currency allowlists require networks.')
|
|
132
132
|
if (currencies) {
|
|
133
133
|
const acceptedCurrency = getAddress(request.currency as `0x${string}`)
|
|
134
134
|
const allowed = currencies.some((currency) =>
|
|
135
|
-
|
|
135
|
+
Assets.matches(currency, acceptedCurrency, network),
|
|
136
136
|
)
|
|
137
137
|
if (!allowed) throw new Error(`EVM currency is not allowed: ${acceptedCurrency}.`)
|
|
138
138
|
}
|
|
@@ -159,30 +159,18 @@ function resolveAuthorization(
|
|
|
159
159
|
const acceptedCurrency = getAddress(request.currency as `0x${string}`)
|
|
160
160
|
const network = Types.networkOf(request.methodDetails.chainId)
|
|
161
161
|
const currency = currencies?.find((currency) =>
|
|
162
|
-
|
|
162
|
+
Assets.matches(currency, acceptedCurrency, network),
|
|
163
163
|
)
|
|
164
|
-
|
|
164
|
+
const resolved = currency ? Assets.resolve(currency, network) : undefined
|
|
165
|
+
if (resolved?.transfer?.type === Types.eip3009)
|
|
165
166
|
return {
|
|
166
|
-
name:
|
|
167
|
-
version:
|
|
167
|
+
name: resolved.transfer.name,
|
|
168
|
+
version: resolved.transfer.version,
|
|
168
169
|
}
|
|
169
170
|
if (parameters.authorization) return parameters.authorization
|
|
170
171
|
throw new Error('EVM authorization requires token name and version.')
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
function addressOf(currency: `0x${string}` | Assets.KnownAsset): `0x${string}` {
|
|
174
|
-
return Assets.isAsset(currency) ? currency.address : currency
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function currencyMatches(
|
|
178
|
-
currency: `0x${string}` | Assets.KnownAsset,
|
|
179
|
-
acceptedCurrency: `0x${string}`,
|
|
180
|
-
network: Types.EvmNetwork,
|
|
181
|
-
): boolean {
|
|
182
|
-
if (getAddress(addressOf(currency)) !== acceptedCurrency) return false
|
|
183
|
-
return !Assets.isAsset(currency) || currency.network === network
|
|
184
|
-
}
|
|
185
|
-
|
|
186
174
|
function decimalsOfAcceptedCurrency(
|
|
187
175
|
parameters: charge.Parameters,
|
|
188
176
|
request: Types.ChargeRequest,
|
|
@@ -191,8 +179,9 @@ function decimalsOfAcceptedCurrency(
|
|
|
191
179
|
const acceptedCurrency = getAddress(request.currency as `0x${string}`)
|
|
192
180
|
const network = Types.networkOf(request.methodDetails.chainId)
|
|
193
181
|
const currency = currencies?.find((currency) =>
|
|
194
|
-
|
|
182
|
+
Assets.matches(currency, acceptedCurrency, network),
|
|
195
183
|
)
|
|
196
|
-
|
|
184
|
+
const resolved = currency ? Assets.resolve(currency, network) : undefined
|
|
185
|
+
if (resolved?.decimals !== undefined) return resolved.decimals
|
|
197
186
|
return parameters.decimals
|
|
198
187
|
}
|
|
@@ -4,6 +4,7 @@ import { Types as evm_Types } from 'mppx/evm'
|
|
|
4
4
|
import { evm, Mppx } from 'mppx/server'
|
|
5
5
|
import { Header as x402_Header, Types as x402_Types, type PaymentPayload } from 'mppx/x402'
|
|
6
6
|
import { privateKeyToAccount } from 'viem/accounts'
|
|
7
|
+
import { usdc } from 'viem/tokens'
|
|
7
8
|
import { describe, expect, test } from 'vp/test'
|
|
8
9
|
|
|
9
10
|
const currency = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'
|
|
@@ -14,6 +15,145 @@ const account = privateKeyToAccount(
|
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
describe('evm charge server', () => {
|
|
18
|
+
test('resolves viem token currency config', async () => {
|
|
19
|
+
const mppx = Mppx.create({
|
|
20
|
+
methods: [
|
|
21
|
+
evm({
|
|
22
|
+
authorization: { name: 'USD Coin', version: '2' },
|
|
23
|
+
chainId: 84532,
|
|
24
|
+
currency: usdc,
|
|
25
|
+
recipient,
|
|
26
|
+
x402: {
|
|
27
|
+
facilitator: {
|
|
28
|
+
async verify() {
|
|
29
|
+
return { isValid: true }
|
|
30
|
+
},
|
|
31
|
+
async settle() {
|
|
32
|
+
return {
|
|
33
|
+
network: evm_Types.networkOf(84532),
|
|
34
|
+
success: true,
|
|
35
|
+
transaction,
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
secretKey: 'test-secret-key-test-secret-key-32',
|
|
43
|
+
})
|
|
44
|
+
const route = mppx.evm.charge({ amount: '0.25' })
|
|
45
|
+
|
|
46
|
+
const first = await route(new Request('https://example.com/paid'))
|
|
47
|
+
|
|
48
|
+
expect(first.status).toBe(402)
|
|
49
|
+
if (first.status !== 402) throw new Error()
|
|
50
|
+
const challenge = Challenge.fromResponse(first.challenge)
|
|
51
|
+
expect(challenge.request).toEqual({
|
|
52
|
+
amount: '250000',
|
|
53
|
+
currency,
|
|
54
|
+
methodDetails: {
|
|
55
|
+
chainId: 84532,
|
|
56
|
+
credentialTypes: ['authorization'],
|
|
57
|
+
decimals: 6,
|
|
58
|
+
},
|
|
59
|
+
recipient,
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('requires chain ID for viem token currency config', () => {
|
|
64
|
+
expect(() =>
|
|
65
|
+
evm({
|
|
66
|
+
authorization: { name: 'USD Coin', version: '2' },
|
|
67
|
+
currency: usdc,
|
|
68
|
+
recipient,
|
|
69
|
+
settle: async () => ({ reference: transaction }),
|
|
70
|
+
}),
|
|
71
|
+
).toThrow('EVM authorization requires `chainId`.')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('rejects viem token currency unavailable on configured chain', () => {
|
|
75
|
+
expect(() =>
|
|
76
|
+
evm({
|
|
77
|
+
authorization: { name: 'USD Coin', version: '2' },
|
|
78
|
+
chainId: 999_999,
|
|
79
|
+
currency: usdc,
|
|
80
|
+
recipient,
|
|
81
|
+
settle: async () => ({ reference: transaction }),
|
|
82
|
+
}),
|
|
83
|
+
).toThrow('EVM currency is not available on chain ID 999999.')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('requires authorization metadata for viem token currency config', () => {
|
|
87
|
+
expect(() =>
|
|
88
|
+
evm({
|
|
89
|
+
chainId: 84532,
|
|
90
|
+
currency: usdc,
|
|
91
|
+
recipient,
|
|
92
|
+
settle: async () => ({ reference: transaction }),
|
|
93
|
+
}),
|
|
94
|
+
).toThrow('EVM authorization requires `authorization` metadata.')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('rejects known asset currency configured for a different chain', () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
evm({
|
|
100
|
+
chainId: 8453,
|
|
101
|
+
currency: evm.assets.baseSepolia.USDC,
|
|
102
|
+
recipient,
|
|
103
|
+
settle: async () => ({ reference: transaction }),
|
|
104
|
+
}),
|
|
105
|
+
).toThrow('EVM currency is not available on chain ID 8453.')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('infers native charge defaults from known asset metadata', async () => {
|
|
109
|
+
const mppx = Mppx.create({
|
|
110
|
+
methods: [
|
|
111
|
+
evm({
|
|
112
|
+
currency: evm.assets.base.USDC,
|
|
113
|
+
recipient,
|
|
114
|
+
settle: async () => ({ reference: transaction }),
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
secretKey: 'test-secret-key-test-secret-key-32',
|
|
118
|
+
})
|
|
119
|
+
const route = mppx.evm.charge({ amount: '0.25' })
|
|
120
|
+
|
|
121
|
+
const response = await route(new Request('https://example.com/paid'))
|
|
122
|
+
|
|
123
|
+
expect(response.status).toBe(402)
|
|
124
|
+
if (response.status !== 402) throw new Error()
|
|
125
|
+
const challenge = Challenge.fromResponse(response.challenge)
|
|
126
|
+
expect(challenge.request).toEqual({
|
|
127
|
+
amount: '250000',
|
|
128
|
+
currency: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
129
|
+
methodDetails: {
|
|
130
|
+
chainId: 8453,
|
|
131
|
+
credentialTypes: ['authorization'],
|
|
132
|
+
decimals: 6,
|
|
133
|
+
},
|
|
134
|
+
recipient,
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('requires authorization metadata for known assets without EIP-3009 transfer metadata', () => {
|
|
139
|
+
const permit2Asset = evm.assets.define({
|
|
140
|
+
address: currency,
|
|
141
|
+
decimals: 6,
|
|
142
|
+
network: 'eip155:84532',
|
|
143
|
+
transfer: {
|
|
144
|
+
type: 'permit2',
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(() =>
|
|
149
|
+
evm({
|
|
150
|
+
currency: permit2Asset,
|
|
151
|
+
recipient,
|
|
152
|
+
settle: async () => ({ reference: transaction }),
|
|
153
|
+
}),
|
|
154
|
+
).toThrow('EVM authorization requires `authorization` metadata.')
|
|
155
|
+
})
|
|
156
|
+
|
|
17
157
|
test('settles native Payment-auth authorization credentials', async () => {
|
|
18
158
|
let facilitated:
|
|
19
159
|
| {
|
package/src/evm/server/Charge.ts
CHANGED
|
@@ -110,11 +110,11 @@ export declare namespace charge {
|
|
|
110
110
|
type NativeConfig = BaseConfig & CurrencyConfig & RecipientConfig
|
|
111
111
|
|
|
112
112
|
type BaseConfig = {
|
|
113
|
-
/** EIP-3009 token domain metadata. Required for
|
|
113
|
+
/** EIP-3009 token domain metadata. Required for raw addresses and viem tokens; inferred for known assets. */
|
|
114
114
|
authorization?: Types.AuthorizationConfig | undefined
|
|
115
|
-
/** EVM chain ID. Required for
|
|
115
|
+
/** EVM chain ID. Required for raw addresses and viem tokens; inferred for known assets. */
|
|
116
116
|
chainId?: number | undefined
|
|
117
|
-
/** Token decimal places. Required for
|
|
117
|
+
/** Token decimal places. Required for raw addresses; inferred for known assets and viem tokens. */
|
|
118
118
|
decimals?: number | undefined
|
|
119
119
|
/** Custom settlement override. If omitted, `x402.facilitator` is used. */
|
|
120
120
|
settle?: SettleAuthorization | undefined
|
|
@@ -123,8 +123,8 @@ export declare namespace charge {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
type CurrencyConfig = {
|
|
126
|
-
/** Token contract address
|
|
127
|
-
currency:
|
|
126
|
+
/** Token contract address, known EVM asset metadata, or viem token definition. */
|
|
127
|
+
currency: Assets.Currency
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
type RecipientConfig = {
|
|
@@ -197,17 +197,24 @@ function resolveConfig(config: charge.NativeConfig): ResolvedConfig {
|
|
|
197
197
|
let decimals = config.decimals
|
|
198
198
|
|
|
199
199
|
if (Assets.isAsset(currency)) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
chainId ??= Assets.toChainId(currency.network)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (chainId !== undefined) {
|
|
204
|
+
const resolved = Assets.resolve(currency, Types.networkOf(chainId))
|
|
205
|
+
if (!resolved) throw new Error(`EVM currency is not available on chain ID ${chainId}.`)
|
|
206
|
+
address = resolved.address
|
|
207
|
+
decimals ??= resolved.decimals
|
|
208
|
+
if (resolved.transfer?.type === Types.eip3009) {
|
|
204
209
|
authorization ??= {
|
|
205
|
-
name:
|
|
206
|
-
version:
|
|
210
|
+
name: resolved.transfer.name,
|
|
211
|
+
version: resolved.transfer.version,
|
|
207
212
|
}
|
|
208
213
|
}
|
|
209
|
-
} else {
|
|
214
|
+
} else if (Assets.isRawAddress(currency)) {
|
|
210
215
|
address = currency
|
|
216
|
+
} else {
|
|
217
|
+
throw new Error('EVM authorization requires `chainId`.')
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
if (!authorization) throw new Error('EVM authorization requires `authorization` metadata.')
|
|
@@ -109,18 +109,24 @@ describe('findCalls', () => {
|
|
|
109
109
|
const tokenOut = '0x2222222222222222222222222222222222222222' as Address
|
|
110
110
|
const tokenIn = '0x3333333333333333333333333333333333333333' as Address
|
|
111
111
|
const client = { chain: { id: 42431 } }
|
|
112
|
-
const
|
|
112
|
+
const builderCalls: { client: unknown; name: string; parameters: Record<string, unknown> }[] =
|
|
113
|
+
[]
|
|
113
114
|
|
|
114
115
|
function getBalanceCall(client: unknown, parameters: Record<string, unknown>) {
|
|
115
|
-
|
|
116
|
+
builderCalls.push({ client, name: 'getBalance', parameters })
|
|
116
117
|
return { kind: parameters.token === tokenOut ? 'targetBalance' : 'candidateBalance' }
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
function approveCall(client: unknown, parameters: Record<string, unknown>) {
|
|
120
|
-
|
|
121
|
+
builderCalls.push({ client, name: 'approve', parameters })
|
|
121
122
|
return { kind: 'approve' }
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
function buyCall(client: unknown, parameters: Record<string, unknown>) {
|
|
126
|
+
builderCalls.push({ client, name: 'buy', parameters })
|
|
127
|
+
return { kind: 'buy' }
|
|
128
|
+
}
|
|
129
|
+
|
|
124
130
|
vi.doMock('viem/actions', () => ({
|
|
125
131
|
readContract: vi.fn(async (_client, call: { kind: string }) =>
|
|
126
132
|
call.kind === 'targetBalance' ? 0n : 2_000_000n,
|
|
@@ -129,7 +135,7 @@ describe('findCalls', () => {
|
|
|
129
135
|
vi.doMock('viem/tempo', () => ({
|
|
130
136
|
Actions: {
|
|
131
137
|
dex: {
|
|
132
|
-
buy: { call:
|
|
138
|
+
buy: { call: buyCall },
|
|
133
139
|
getBuyQuote: vi.fn(async () => 1_000_000n),
|
|
134
140
|
},
|
|
135
141
|
token: {
|
|
@@ -152,15 +158,26 @@ describe('findCalls', () => {
|
|
|
152
158
|
})
|
|
153
159
|
|
|
154
160
|
expect(calls).toHaveLength(2)
|
|
155
|
-
expect(
|
|
156
|
-
expect(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
expect(builderCalls.map((call) => call.client)).toEqual([client, client, client, client])
|
|
162
|
+
expect(builderCalls.map((call) => call.name)).toEqual([
|
|
163
|
+
'getBalance',
|
|
164
|
+
'getBalance',
|
|
165
|
+
'approve',
|
|
166
|
+
'buy',
|
|
167
|
+
])
|
|
168
|
+
expect(builderCalls[0]!.parameters).toMatchObject({ account, token: tokenOut })
|
|
169
|
+
expect(builderCalls[1]!.parameters).toMatchObject({ account, token: tokenIn })
|
|
170
|
+
expect(builderCalls[2]!.parameters).toMatchObject({
|
|
160
171
|
amount: 1_010_000n,
|
|
161
172
|
spender: '0x4444444444444444444444444444444444444444',
|
|
162
173
|
token: tokenIn,
|
|
163
174
|
})
|
|
175
|
+
expect(builderCalls[3]!.parameters).toMatchObject({
|
|
176
|
+
amountOut: 1_000_000n,
|
|
177
|
+
maxAmountIn: 1_010_000n,
|
|
178
|
+
tokenIn,
|
|
179
|
+
tokenOut,
|
|
180
|
+
})
|
|
164
181
|
} finally {
|
|
165
182
|
vi.doUnmock('viem/actions')
|
|
166
183
|
vi.doUnmock('viem/tempo')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Address, Client } from 'viem'
|
|
2
2
|
import { readContract } from 'viem/actions'
|
|
3
3
|
import { Actions, Addresses } from 'viem/tempo'
|
|
4
|
-
import type { token as viem_token } from 'viem/tempo/actions'
|
|
4
|
+
import type { dex as viem_dex, token as viem_token } from 'viem/tempo/actions'
|
|
5
5
|
|
|
6
6
|
import * as TempoAddress from './address.js'
|
|
7
7
|
import * as defaults from './defaults.js'
|
|
@@ -47,6 +47,19 @@ function getApproveCall(
|
|
|
47
47
|
return call.length >= 2 ? call(client, parameters) : call(parameters)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// TODO: Remove once the minimum viem version is >=2.54.0, which uses client-first Tempo call builders.
|
|
51
|
+
function getBuyCall(
|
|
52
|
+
client: Client,
|
|
53
|
+
parameters: viem_dex.buy.Args,
|
|
54
|
+
): ReturnType<typeof viem_dex.buy.call> {
|
|
55
|
+
const call = Actions.dex.buy.call as unknown as {
|
|
56
|
+
length: number
|
|
57
|
+
(parameters: viem_dex.buy.Args): ReturnType<typeof viem_dex.buy.call>
|
|
58
|
+
(client: Client, parameters: viem_dex.buy.Args): ReturnType<typeof viem_dex.buy.call>
|
|
59
|
+
}
|
|
60
|
+
return call.length >= 2 ? call(client, parameters) : call(parameters)
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
/**
|
|
51
64
|
* Finds the optimal swap calls to acquire `amountOut` of `tokenOut`,
|
|
52
65
|
* returning an approve + buy call sequence if a viable route is found.
|
|
@@ -100,7 +113,7 @@ export async function findCalls(
|
|
|
100
113
|
spender: Addresses.stablecoinDex,
|
|
101
114
|
token: tokenIn,
|
|
102
115
|
}),
|
|
103
|
-
|
|
116
|
+
getBuyCall(client, {
|
|
104
117
|
tokenIn,
|
|
105
118
|
tokenOut,
|
|
106
119
|
amountOut,
|