mppx 0.6.27 → 0.6.29
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 +25 -0
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +16 -10
- package/dist/Challenge.js.map +1 -1
- package/dist/Method.d.ts +1 -1
- package/dist/Method.d.ts.map +1 -1
- package/dist/Store.d.ts +32 -9
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +42 -10
- package/dist/Store.js.map +1 -1
- package/dist/client/Methods.d.ts +1 -0
- package/dist/client/Methods.d.ts.map +1 -1
- package/dist/client/Methods.js +1 -0
- package/dist/client/Methods.js.map +1 -1
- package/dist/client/Mppx.d.ts +3 -3
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +1 -0
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/Transport.d.ts +10 -3
- package/dist/client/Transport.d.ts.map +1 -1
- package/dist/client/Transport.js +60 -7
- package/dist/client/Transport.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts +3 -0
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +12 -20
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/evm/Assets.d.ts +2 -0
- package/dist/evm/Assets.d.ts.map +1 -0
- package/dist/evm/Assets.js +2 -0
- package/dist/evm/Assets.js.map +1 -0
- package/dist/evm/Chains.d.ts +5 -0
- package/dist/evm/Chains.d.ts.map +1 -0
- package/dist/evm/Chains.js +5 -0
- package/dist/evm/Chains.js.map +1 -0
- package/dist/evm/Methods.d.ts +68 -0
- package/dist/evm/Methods.d.ts.map +1 -0
- package/dist/evm/Methods.js +28 -0
- package/dist/evm/Methods.js.map +1 -0
- package/dist/evm/Types.d.ts +143 -0
- package/dist/evm/Types.d.ts.map +1 -0
- package/dist/evm/Types.js +102 -0
- package/dist/evm/Types.js.map +1 -0
- package/dist/evm/client/Charge.d.ts +102 -0
- package/dist/evm/client/Charge.d.ts.map +1 -0
- package/dist/evm/client/Charge.js +141 -0
- package/dist/evm/client/Charge.js.map +1 -0
- package/dist/evm/client/Methods.d.ts +81 -0
- package/dist/evm/client/Methods.d.ts.map +1 -0
- package/dist/evm/client/Methods.js +16 -0
- package/dist/evm/client/Methods.js.map +1 -0
- package/dist/evm/client/index.d.ts +6 -0
- package/dist/evm/client/index.d.ts.map +1 -0
- package/dist/evm/client/index.js +6 -0
- package/dist/evm/client/index.js.map +1 -0
- package/dist/evm/index.d.ts +9 -0
- package/dist/evm/index.d.ts.map +1 -0
- package/dist/evm/index.js +8 -0
- package/dist/evm/index.js.map +1 -0
- package/dist/evm/server/Charge.d.ts +62 -0
- package/dist/evm/server/Charge.d.ts.map +1 -0
- package/dist/evm/server/Charge.js +172 -0
- package/dist/evm/server/Charge.js.map +1 -0
- package/dist/evm/server/Methods.d.ts +80 -0
- package/dist/evm/server/Methods.d.ts.map +1 -0
- package/dist/evm/server/Methods.js +16 -0
- package/dist/evm/server/Methods.js.map +1 -0
- package/dist/evm/server/index.d.ts +6 -0
- package/dist/evm/server/index.d.ts.map +1 -0
- package/dist/evm/server/index.js +6 -0
- package/dist/evm/server/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/HeaderCodec.d.ts +18 -0
- package/dist/internal/HeaderCodec.d.ts.map +1 -0
- package/dist/internal/HeaderCodec.js +31 -0
- package/dist/internal/HeaderCodec.js.map +1 -0
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +2 -3
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/middlewares/express.js +2 -1
- package/dist/middlewares/express.js.map +1 -1
- package/dist/proxy/internal/Headers.d.ts +13 -1
- package/dist/proxy/internal/Headers.d.ts.map +1 -1
- package/dist/proxy/internal/Headers.js +25 -2
- package/dist/proxy/internal/Headers.js.map +1 -1
- package/dist/proxy/services/openai.d.ts.map +1 -1
- package/dist/proxy/services/openai.js +2 -0
- package/dist/proxy/services/openai.js.map +1 -1
- package/dist/server/Methods.d.ts +1 -0
- package/dist/server/Methods.d.ts.map +1 -1
- package/dist/server/Methods.js +1 -0
- package/dist/server/Methods.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +90 -12
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +10 -0
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +9 -0
- package/dist/server/Transport.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts +31 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +88 -11
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts +3 -3
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js +13 -6
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +8 -5
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +0 -1
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/client/Session.d.ts +2 -4
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js +10 -12
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +3 -3
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +1 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/account.d.ts +5 -0
- package/dist/tempo/internal/account.d.ts.map +1 -1
- package/dist/tempo/internal/account.js +8 -0
- package/dist/tempo/internal/account.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +5 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +6 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +30 -3
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +6 -0
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +14 -13
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/Subscription.d.ts +6 -0
- package/dist/tempo/server/Subscription.d.ts.map +1 -1
- package/dist/tempo/server/Subscription.js +1 -1
- package/dist/tempo/server/Subscription.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/tempo/session/Chain.d.ts +2 -0
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +8 -8
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts +4 -3
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js +71 -44
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/dist/tempo/session/Ws.d.ts.map +1 -1
- package/dist/tempo/session/Ws.js +15 -0
- package/dist/tempo/session/Ws.js.map +1 -1
- package/dist/tempo/subscription/KeyAuthorization.d.ts +2 -2
- package/dist/x402/Assets.d.ts +29 -0
- package/dist/x402/Assets.d.ts.map +1 -0
- package/dist/x402/Assets.js +46 -0
- package/dist/x402/Assets.js.map +1 -0
- package/dist/x402/Header.d.ts +14 -0
- package/dist/x402/Header.d.ts.map +1 -0
- package/dist/x402/Header.js +18 -0
- package/dist/x402/Header.js.map +1 -0
- package/dist/x402/Types.d.ts +289 -0
- package/dist/x402/Types.d.ts.map +1 -0
- package/dist/x402/Types.js +139 -0
- package/dist/x402/Types.js.map +1 -0
- package/dist/x402/client/Exact.d.ts +38 -0
- package/dist/x402/client/Exact.d.ts.map +1 -0
- package/dist/x402/client/Exact.js +141 -0
- package/dist/x402/client/Exact.js.map +1 -0
- package/dist/x402/index.d.ts +6 -0
- package/dist/x402/index.d.ts.map +1 -0
- package/dist/x402/index.js +6 -0
- package/dist/x402/index.js.map +1 -0
- package/dist/x402/internal/ChallengeBrand.d.ts +5 -0
- package/dist/x402/internal/ChallengeBrand.d.ts.map +1 -0
- package/dist/x402/internal/ChallengeBrand.js +13 -0
- package/dist/x402/internal/ChallengeBrand.js.map +1 -0
- package/dist/x402/internal/RouteBinding.d.ts +8 -0
- package/dist/x402/internal/RouteBinding.d.ts.map +1 -0
- package/dist/x402/internal/RouteBinding.js +12 -0
- package/dist/x402/internal/RouteBinding.js.map +1 -0
- package/dist/x402/server/EvmCharge.d.ts +50 -0
- package/dist/x402/server/EvmCharge.d.ts.map +1 -0
- package/dist/x402/server/EvmCharge.js +301 -0
- package/dist/x402/server/EvmCharge.js.map +1 -0
- package/dist/x402/server/Facilitator.d.ts +12 -0
- package/dist/x402/server/Facilitator.d.ts.map +1 -0
- package/dist/x402/server/Facilitator.js +42 -0
- package/dist/x402/server/Facilitator.js.map +1 -0
- package/package.json +41 -21
- package/src/Challenge.test.ts +28 -0
- package/src/Challenge.ts +17 -10
- package/src/Method.ts +1 -1
- package/src/Store.test-d.ts +58 -0
- package/src/Store.test.ts +77 -0
- package/src/Store.ts +155 -74
- package/src/client/Methods.ts +1 -0
- package/src/client/Mppx.ts +4 -3
- package/src/client/Transport.test.ts +165 -30
- package/src/client/Transport.ts +76 -8
- package/src/client/index.ts +1 -1
- package/src/client/internal/Fetch.test.ts +31 -2
- package/src/client/internal/Fetch.ts +26 -19
- package/src/evm/Assets.ts +1 -0
- package/src/evm/Chains.ts +5 -0
- package/src/evm/Methods.ts +44 -0
- package/src/evm/PublicInterface.test-d.ts +109 -0
- package/src/evm/Types.ts +140 -0
- package/src/evm/client/Charge.test.ts +99 -0
- package/src/evm/client/Charge.ts +198 -0
- package/src/evm/client/Methods.ts +19 -0
- package/src/evm/client/index.ts +5 -0
- package/src/evm/index.ts +13 -0
- package/src/evm/server/Charge.test.ts +199 -0
- package/src/evm/server/Charge.ts +283 -0
- package/src/evm/server/Methods.ts +22 -0
- package/src/evm/server/index.ts +5 -0
- package/src/index.ts +2 -0
- package/src/internal/HeaderCodec.ts +36 -0
- package/src/middlewares/elysia.test.ts +25 -0
- package/src/middlewares/elysia.ts +1 -2
- package/src/middlewares/express.test.ts +28 -0
- package/src/middlewares/express.ts +1 -1
- package/src/middlewares/hono.test.ts +138 -2
- package/src/middlewares/nextjs.test.ts +22 -0
- package/src/proxy/internal/Headers.test.ts +38 -0
- package/src/proxy/internal/Headers.ts +26 -2
- package/src/proxy/services/openai.test.ts +57 -1
- package/src/proxy/services/openai.ts +2 -0
- package/src/server/Methods.ts +1 -0
- package/src/server/Mppx.test.ts +244 -1
- package/src/server/Mppx.ts +124 -11
- package/src/server/NodeListener.test.ts +28 -1
- package/src/server/Transport.test.ts +19 -0
- package/src/server/Transport.ts +20 -0
- package/src/server/index.ts +1 -1
- package/src/stripe/server/Charge.test.ts +215 -1
- package/src/stripe/server/Charge.ts +150 -20
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/AccessKeyAuthorization.test.ts +231 -0
- package/src/tempo/client/ChannelOps.test.ts +61 -7
- package/src/tempo/client/ChannelOps.ts +18 -7
- package/src/tempo/client/Charge.test.ts +126 -0
- package/src/tempo/client/Charge.ts +10 -6
- package/src/tempo/client/Session.test.ts +130 -1
- package/src/tempo/client/Session.ts +12 -19
- package/src/tempo/client/SessionManager.test.ts +69 -2
- package/src/tempo/client/SessionManager.ts +4 -4
- package/src/tempo/internal/account.ts +13 -0
- package/src/tempo/internal/fee-payer.test.ts +32 -2
- package/src/tempo/internal/fee-payer.ts +6 -2
- package/src/tempo/server/Charge.test.ts +91 -1
- package/src/tempo/server/Charge.ts +48 -2
- package/src/tempo/server/Session.test.ts +30 -0
- package/src/tempo/server/Session.ts +24 -17
- package/src/tempo/server/Subscription.ts +7 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/Chain.test.ts +4 -4
- package/src/tempo/session/Chain.ts +10 -6
- package/src/tempo/session/ChannelStore.test.ts +21 -0
- package/src/tempo/session/Voucher.test.ts +230 -1
- package/src/tempo/session/Voucher.ts +96 -48
- package/src/tempo/session/Ws.test.ts +71 -0
- package/src/tempo/session/Ws.ts +13 -0
- package/src/tempo/subscription/Store.test.ts +55 -0
- package/src/x402/Assets.ts +65 -0
- package/src/x402/Exact.e2e.test.ts +448 -0
- package/src/x402/Header.test.ts +73 -0
- package/src/x402/Header.ts +34 -0
- package/src/x402/PublicInterface.test-d.ts +39 -0
- package/src/x402/Types.ts +248 -0
- package/src/x402/client/Exact.test.ts +180 -0
- package/src/x402/client/Exact.ts +198 -0
- package/src/x402/index.ts +5 -0
- package/src/x402/internal/ChallengeBrand.ts +14 -0
- package/src/x402/internal/RouteBinding.ts +18 -0
- package/src/x402/server/EvmCharge.ts +394 -0
- package/src/x402/server/Facilitator.test.ts +111 -0
- package/src/x402/server/Facilitator.ts +56 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { evm, Mppx } from 'mppx/server'
|
|
2
|
+
import type { Account } from 'viem'
|
|
3
|
+
import { describe, expectTypeOf, test } from 'vp/test'
|
|
4
|
+
|
|
5
|
+
import { evm as clientEvm } from '../client/index.js'
|
|
6
|
+
|
|
7
|
+
const secretKey = 'test-secret'
|
|
8
|
+
|
|
9
|
+
describe('x402 public interface', () => {
|
|
10
|
+
test('server evm charge accepts known assets without transfer metadata', () => {
|
|
11
|
+
const mppx = Mppx.create({
|
|
12
|
+
methods: [
|
|
13
|
+
evm.charge({
|
|
14
|
+
currency: evm.assets.base.USDC,
|
|
15
|
+
recipient: '0x209693Bc6afc0C5328bA36FaF03C514EF312287C',
|
|
16
|
+
x402: {
|
|
17
|
+
facilitator: 'https://x402.org/facilitator',
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
20
|
+
],
|
|
21
|
+
secretKey,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expectTypeOf(mppx.evm.charge).toBeFunction()
|
|
25
|
+
expectTypeOf(mppx.evm.charge({ amount: '0.01' })).toBeFunction()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('client evm charge exposes account config and policies', () => {
|
|
29
|
+
const method = clientEvm.charge({
|
|
30
|
+
account: {} as Account,
|
|
31
|
+
currencies: [clientEvm.assets.baseSepolia.USDC],
|
|
32
|
+
maxAmount: '0.01',
|
|
33
|
+
networks: [clientEvm.chains.baseSepolia],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expectTypeOf(method.intent).toEqualTypeOf<'charge'>()
|
|
37
|
+
expectTypeOf(method.name).toEqualTypeOf<'evm'>()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as z from '../zod.js'
|
|
2
|
+
|
|
3
|
+
export const versions = [2] as const
|
|
4
|
+
|
|
5
|
+
/** mppx method name used for EVM charge challenges backed by x402. */
|
|
6
|
+
export const paymentMethod = 'evm' as const
|
|
7
|
+
|
|
8
|
+
/** mppx intent name used for EVM charge challenges backed by x402 exact. */
|
|
9
|
+
export const exactIntent = 'charge' as const
|
|
10
|
+
|
|
11
|
+
export const schemes = ['exact'] as const
|
|
12
|
+
export const assetTransferMethods = ['eip3009', 'permit2'] as const
|
|
13
|
+
|
|
14
|
+
/** CAIP-2 namespace prefix for EVM networks. */
|
|
15
|
+
export const evmNetworkPrefix = 'eip155:' as const
|
|
16
|
+
|
|
17
|
+
/** Prefix for synthetic mppx challenge IDs derived from x402 `accepts` entries. */
|
|
18
|
+
export const syntheticChallengeIdPrefix = 'x402:' as const
|
|
19
|
+
|
|
20
|
+
/** x402 protocol version supported by this package. */
|
|
21
|
+
export type Version = 2
|
|
22
|
+
|
|
23
|
+
/** mppx payment method name used for EVM charge challenges backed by x402. */
|
|
24
|
+
export type PaymentMethod = typeof paymentMethod
|
|
25
|
+
|
|
26
|
+
/** mppx intent name used for EVM charge challenges backed by x402 exact. */
|
|
27
|
+
export type ExactIntent = typeof exactIntent
|
|
28
|
+
|
|
29
|
+
/** x402 scheme supported by this package. */
|
|
30
|
+
export type Scheme = (typeof schemes)[number]
|
|
31
|
+
|
|
32
|
+
/** x402 exact EVM asset transfer method. */
|
|
33
|
+
export type AssetTransferMethod = (typeof assetTransferMethods)[number]
|
|
34
|
+
|
|
35
|
+
/** CAIP-2 EVM network identifier. */
|
|
36
|
+
export type EvmNetwork = `${typeof evmNetworkPrefix}${number}`
|
|
37
|
+
|
|
38
|
+
/** HTTP header carrying a base64-encoded x402 payment-required response. */
|
|
39
|
+
export const paymentRequiredHeader = 'PAYMENT-REQUIRED'
|
|
40
|
+
|
|
41
|
+
/** HTTP header carrying a base64-encoded x402 payment payload. */
|
|
42
|
+
export const paymentSignatureHeader = 'PAYMENT-SIGNATURE'
|
|
43
|
+
|
|
44
|
+
/** HTTP header carrying a base64-encoded x402 settlement response. */
|
|
45
|
+
export const paymentResponseHeader = 'PAYMENT-RESPONSE'
|
|
46
|
+
|
|
47
|
+
const nonEmptyString = z.string().check(z.minLength(1))
|
|
48
|
+
const positiveNumber = z.number().check(z.refine((value) => value > 0, 'Must be positive'))
|
|
49
|
+
const atomicAmount = z.string().check(z.regex(/^\d+$/, 'Invalid atomic amount'))
|
|
50
|
+
const address = z.address()
|
|
51
|
+
const evmNetwork = z
|
|
52
|
+
.string()
|
|
53
|
+
.check(
|
|
54
|
+
z.regex(new RegExp(`^${evmNetworkPrefix}\\d+$`), 'Invalid EVM CAIP-2 network'),
|
|
55
|
+
) as z.ZodMiniType<EvmNetwork>
|
|
56
|
+
|
|
57
|
+
/** Describes the protected resource in x402 v2 payment-required responses. */
|
|
58
|
+
export const ResourceInfoSchema = z.object({
|
|
59
|
+
description: z.optional(z.string()),
|
|
60
|
+
iconUrl: z.optional(z.string()),
|
|
61
|
+
mimeType: z.optional(z.string()),
|
|
62
|
+
serviceName: z.optional(z.string()),
|
|
63
|
+
tags: z.optional(z.array(z.string())),
|
|
64
|
+
url: nonEmptyString,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/** Describes the protected resource in x402 v2 payment-required responses. */
|
|
68
|
+
export type ResourceInfo = z.infer<typeof ResourceInfoSchema>
|
|
69
|
+
|
|
70
|
+
/** Public transfer configuration for exact EVM payments. */
|
|
71
|
+
export const ExactTransferSchema = z.discriminatedUnion('type', [
|
|
72
|
+
z.object({
|
|
73
|
+
name: nonEmptyString,
|
|
74
|
+
type: z.literal('eip3009'),
|
|
75
|
+
version: nonEmptyString,
|
|
76
|
+
}),
|
|
77
|
+
z.object({
|
|
78
|
+
name: z.optional(z.string()),
|
|
79
|
+
type: z.literal('permit2'),
|
|
80
|
+
version: z.optional(z.string()),
|
|
81
|
+
}),
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
/** Public EIP-3009 transfer configuration for exact EVM payments. */
|
|
85
|
+
export type ExactEip3009Transfer = Extract<z.infer<typeof ExactTransferSchema>, { type: 'eip3009' }>
|
|
86
|
+
|
|
87
|
+
/** Public Permit2 transfer configuration for exact EVM payments. */
|
|
88
|
+
export type ExactPermit2Transfer = Extract<z.infer<typeof ExactTransferSchema>, { type: 'permit2' }>
|
|
89
|
+
|
|
90
|
+
/** Public transfer configuration for exact EVM payments. */
|
|
91
|
+
export type ExactTransfer = z.infer<typeof ExactTransferSchema>
|
|
92
|
+
|
|
93
|
+
/** Known asset metadata used to derive x402 wire `extra` fields. */
|
|
94
|
+
export type Asset = {
|
|
95
|
+
address: `0x${string}`
|
|
96
|
+
decimals: number
|
|
97
|
+
transfer: ExactTransfer
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** x402 v2 payment requirements for the `exact` scheme. */
|
|
101
|
+
export const PaymentRequirementsSchema = z.object({
|
|
102
|
+
amount: atomicAmount,
|
|
103
|
+
asset: nonEmptyString,
|
|
104
|
+
extra: z.optional(z.record(z.string(), z.unknown())),
|
|
105
|
+
maxTimeoutSeconds: positiveNumber,
|
|
106
|
+
network: evmNetwork,
|
|
107
|
+
payTo: nonEmptyString,
|
|
108
|
+
scheme: z.enum(schemes),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
/** x402 v2 payment requirements for the `exact` scheme. */
|
|
112
|
+
export type PaymentRequirements = z.infer<typeof PaymentRequirementsSchema>
|
|
113
|
+
|
|
114
|
+
/** x402 v2 protocol extension value. */
|
|
115
|
+
export const ExtensionSchema = z.object({
|
|
116
|
+
info: z.record(z.string(), z.unknown()),
|
|
117
|
+
schema: z.record(z.string(), z.unknown()),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
/** x402 v2 protocol extension value. */
|
|
121
|
+
export type Extension = z.infer<typeof ExtensionSchema>
|
|
122
|
+
|
|
123
|
+
/** x402 v2 protocol extensions map. */
|
|
124
|
+
export const ExtensionsSchema = z.record(z.string(), ExtensionSchema)
|
|
125
|
+
|
|
126
|
+
/** x402 v2 protocol extensions map. */
|
|
127
|
+
export type Extensions = z.infer<typeof ExtensionsSchema>
|
|
128
|
+
|
|
129
|
+
/** Public exact EVM route request accepted by `mppx` handlers. */
|
|
130
|
+
export type ExactRequest = PaymentRequirements & {
|
|
131
|
+
extensions?: Extensions | undefined
|
|
132
|
+
resource?: ResourceInfo | undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** x402 v2 payment-required response. */
|
|
136
|
+
export const PaymentRequiredSchema = z.object({
|
|
137
|
+
accepts: z.array(PaymentRequirementsSchema).check(z.minLength(1)),
|
|
138
|
+
error: z.optional(z.string()),
|
|
139
|
+
extensions: z.optional(ExtensionsSchema),
|
|
140
|
+
resource: ResourceInfoSchema,
|
|
141
|
+
x402Version: z.literal(2),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
/** x402 v2 payment-required response. */
|
|
145
|
+
export type PaymentRequired = z.infer<typeof PaymentRequiredSchema>
|
|
146
|
+
|
|
147
|
+
/** EIP-3009 transferWithAuthorization payload for exact EVM payments. */
|
|
148
|
+
export const ExactEip3009PayloadSchema = z.object({
|
|
149
|
+
authorization: z.object({
|
|
150
|
+
from: address,
|
|
151
|
+
nonce: z.hash(),
|
|
152
|
+
to: address,
|
|
153
|
+
validAfter: atomicAmount,
|
|
154
|
+
validBefore: atomicAmount,
|
|
155
|
+
value: atomicAmount,
|
|
156
|
+
}),
|
|
157
|
+
signature: z.signature(),
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
/** EIP-3009 transferWithAuthorization payload for exact EVM payments. */
|
|
161
|
+
export type ExactEip3009Payload = z.infer<typeof ExactEip3009PayloadSchema>
|
|
162
|
+
|
|
163
|
+
/** Permit2 payload for exact EVM payments. */
|
|
164
|
+
export const ExactPermit2PayloadSchema = z.object({
|
|
165
|
+
permit2Authorization: z.object({
|
|
166
|
+
deadline: atomicAmount,
|
|
167
|
+
from: address,
|
|
168
|
+
nonce: atomicAmount,
|
|
169
|
+
permitted: z.object({
|
|
170
|
+
amount: atomicAmount,
|
|
171
|
+
token: address,
|
|
172
|
+
}),
|
|
173
|
+
spender: address,
|
|
174
|
+
witness: z.object({
|
|
175
|
+
to: address,
|
|
176
|
+
validAfter: atomicAmount,
|
|
177
|
+
}),
|
|
178
|
+
}),
|
|
179
|
+
signature: z.signature(),
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
/** Permit2 payload for exact EVM payments. */
|
|
183
|
+
export type ExactPermit2Payload = z.infer<typeof ExactPermit2PayloadSchema>
|
|
184
|
+
|
|
185
|
+
/** Exact EVM payment payload body. */
|
|
186
|
+
export const ExactPayloadSchema = z.union([ExactEip3009PayloadSchema, ExactPermit2PayloadSchema])
|
|
187
|
+
|
|
188
|
+
/** Exact EVM payment payload body. */
|
|
189
|
+
export type ExactPayload = z.infer<typeof ExactPayloadSchema>
|
|
190
|
+
|
|
191
|
+
/** x402 v2 payment payload. */
|
|
192
|
+
export const PaymentPayloadSchema = z.object({
|
|
193
|
+
accepted: PaymentRequirementsSchema,
|
|
194
|
+
extensions: z.optional(ExtensionsSchema),
|
|
195
|
+
payload: ExactPayloadSchema,
|
|
196
|
+
resource: z.optional(ResourceInfoSchema),
|
|
197
|
+
x402Version: z.literal(2),
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
/** x402 v2 payment payload. */
|
|
201
|
+
export type PaymentPayload = z.infer<typeof PaymentPayloadSchema>
|
|
202
|
+
|
|
203
|
+
/** Facilitator verification response. */
|
|
204
|
+
export const VerifyResponseSchema = z.object({
|
|
205
|
+
extensions: z.optional(ExtensionsSchema),
|
|
206
|
+
extra: z.optional(z.record(z.string(), z.unknown())),
|
|
207
|
+
invalidMessage: z.optional(z.string()),
|
|
208
|
+
invalidReason: z.optional(z.string()),
|
|
209
|
+
isValid: z.boolean(),
|
|
210
|
+
payer: z.optional(z.string()),
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
/** Facilitator verification response. */
|
|
214
|
+
export type VerifyResponse = z.infer<typeof VerifyResponseSchema>
|
|
215
|
+
|
|
216
|
+
/** Facilitator settlement response and x402 `PAYMENT-RESPONSE` body. */
|
|
217
|
+
export const SettleResponseSchema = z.object({
|
|
218
|
+
amount: z.optional(atomicAmount),
|
|
219
|
+
errorMessage: z.optional(z.string()),
|
|
220
|
+
errorReason: z.optional(z.string()),
|
|
221
|
+
extensions: z.optional(ExtensionsSchema),
|
|
222
|
+
extra: z.optional(z.record(z.string(), z.unknown())),
|
|
223
|
+
network: nonEmptyString,
|
|
224
|
+
payer: z.optional(z.string()),
|
|
225
|
+
success: z.boolean(),
|
|
226
|
+
transaction: z.string(),
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
/** Facilitator settlement response and x402 `PAYMENT-RESPONSE` body. */
|
|
230
|
+
export type SettleResponse = z.infer<typeof SettleResponseSchema>
|
|
231
|
+
|
|
232
|
+
/** x402 facilitator client interface used by server exact config. */
|
|
233
|
+
export type Facilitator = {
|
|
234
|
+
settle: (
|
|
235
|
+
paymentPayload: PaymentPayload,
|
|
236
|
+
paymentRequirements: PaymentRequirements,
|
|
237
|
+
) => Promise<SettleResponse>
|
|
238
|
+
verify: (
|
|
239
|
+
paymentPayload: PaymentPayload,
|
|
240
|
+
paymentRequirements: PaymentRequirements,
|
|
241
|
+
) => Promise<VerifyResponse>
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Extracts x402 `PaymentRequirements` from a canonical exact request. */
|
|
245
|
+
export function toPaymentRequirements(request: ExactRequest): PaymentRequirements {
|
|
246
|
+
const { extensions: _extensions, resource: _resource, ...paymentRequirements } = request
|
|
247
|
+
return PaymentRequirementsSchema.parse(paymentRequirements)
|
|
248
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Challenge } from 'mppx'
|
|
2
|
+
import type { Account } from 'viem'
|
|
3
|
+
import { describe, expect, test, vi } from 'vp/test'
|
|
4
|
+
|
|
5
|
+
import * as Chains from '../../evm/Chains.js'
|
|
6
|
+
import * as Assets from '../Assets.js'
|
|
7
|
+
import * as Header from '../Header.js'
|
|
8
|
+
import * as RouteBinding from '../internal/RouteBinding.js'
|
|
9
|
+
import * as Types from '../Types.js'
|
|
10
|
+
import { createCredential } from './Exact.js'
|
|
11
|
+
|
|
12
|
+
type X402Challenge = Parameters<typeof createCredential>[0]['challenge']
|
|
13
|
+
|
|
14
|
+
const account = {
|
|
15
|
+
address: '0x1111111111111111111111111111111111111111',
|
|
16
|
+
signTypedData: vi.fn(async () => '0x1234'),
|
|
17
|
+
} as unknown as Account
|
|
18
|
+
const usdc = Assets.define({
|
|
19
|
+
address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
20
|
+
decimals: 6,
|
|
21
|
+
network: 'eip155:84532',
|
|
22
|
+
transfer: {
|
|
23
|
+
name: 'USDC',
|
|
24
|
+
type: 'eip3009',
|
|
25
|
+
version: '2',
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('x402 exact credential helper', () => {
|
|
30
|
+
test('enforces max amount, network, and currency policy before signing', async () => {
|
|
31
|
+
const config = {
|
|
32
|
+
account,
|
|
33
|
+
currencies: [usdc],
|
|
34
|
+
maxAmount: '0.01',
|
|
35
|
+
networks: [Chains.baseSepolia],
|
|
36
|
+
} as const
|
|
37
|
+
|
|
38
|
+
await expect(
|
|
39
|
+
createCredential({
|
|
40
|
+
challenge: challenge({ amount: '10001' }),
|
|
41
|
+
config,
|
|
42
|
+
context: {},
|
|
43
|
+
}),
|
|
44
|
+
).rejects.toThrow('x402 exact amount exceeds maxAmount.')
|
|
45
|
+
|
|
46
|
+
await expect(
|
|
47
|
+
createCredential({
|
|
48
|
+
challenge: challenge({ network: 'eip155:8453' }),
|
|
49
|
+
config,
|
|
50
|
+
context: {},
|
|
51
|
+
}),
|
|
52
|
+
).rejects.toThrow('x402 exact chain ID is not allowed: 8453.')
|
|
53
|
+
|
|
54
|
+
await expect(
|
|
55
|
+
createCredential({
|
|
56
|
+
challenge: challenge({ asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' }),
|
|
57
|
+
config,
|
|
58
|
+
context: {},
|
|
59
|
+
}),
|
|
60
|
+
).rejects.toThrow(
|
|
61
|
+
'x402 exact currency is not allowed: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913.',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
expect(account.signTypedData).not.toHaveBeenCalled()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('requires network policy for raw hex currencies', async () => {
|
|
68
|
+
await expect(
|
|
69
|
+
createCredential({
|
|
70
|
+
challenge: challenge(),
|
|
71
|
+
config: {
|
|
72
|
+
account,
|
|
73
|
+
currencies: [usdc.address],
|
|
74
|
+
maxAmount: '0.01',
|
|
75
|
+
},
|
|
76
|
+
context: {},
|
|
77
|
+
}),
|
|
78
|
+
).rejects.toThrow('x402 exact raw currency allowlists require networks.')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('signs EIP-3009 exact payment payloads', async () => {
|
|
82
|
+
const signTypedData = vi.fn(async () => '0x1234')
|
|
83
|
+
const config = {
|
|
84
|
+
account: {
|
|
85
|
+
...account,
|
|
86
|
+
signTypedData,
|
|
87
|
+
} as unknown as Account,
|
|
88
|
+
currencies: [usdc],
|
|
89
|
+
maxAmount: '0.01',
|
|
90
|
+
networks: [Chains.baseSepolia],
|
|
91
|
+
} as const
|
|
92
|
+
|
|
93
|
+
const credential = await createCredential({
|
|
94
|
+
challenge: challenge(),
|
|
95
|
+
config,
|
|
96
|
+
context: {},
|
|
97
|
+
})
|
|
98
|
+
const paymentPayload = Header.decodePaymentSignature(credential)
|
|
99
|
+
|
|
100
|
+
expect(signTypedData).toHaveBeenCalledOnce()
|
|
101
|
+
expect(paymentPayload.x402Version).toBe(2)
|
|
102
|
+
expect(paymentPayload.accepted.scheme).toBe('exact')
|
|
103
|
+
expect('authorization' in paymentPayload.payload).toBe(true)
|
|
104
|
+
if (!('authorization' in paymentPayload.payload)) throw new Error()
|
|
105
|
+
expect(paymentPayload.extensions?.mppx?.info.method).toBe('GET')
|
|
106
|
+
expect(paymentPayload.extensions?.mppx?.info.nonce).toMatch(/^[0-9a-f]{64}$/)
|
|
107
|
+
expect(paymentPayload.payload.authorization.nonce).toBe(
|
|
108
|
+
RouteBinding.nonce({
|
|
109
|
+
accepted: paymentPayload.accepted,
|
|
110
|
+
extensions: paymentPayload.extensions!,
|
|
111
|
+
resource: paymentPayload.resource!,
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
expect(paymentPayload.payload.signature).toBe('0x1234')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('uses a fresh route-bound nonce for repeated payments', async () => {
|
|
118
|
+
const config = {
|
|
119
|
+
account,
|
|
120
|
+
currencies: [usdc],
|
|
121
|
+
maxAmount: '0.01',
|
|
122
|
+
networks: [Chains.baseSepolia],
|
|
123
|
+
} as const
|
|
124
|
+
|
|
125
|
+
const first = Header.decodePaymentSignature(
|
|
126
|
+
await createCredential({
|
|
127
|
+
challenge: challenge(),
|
|
128
|
+
config,
|
|
129
|
+
context: {},
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
const second = Header.decodePaymentSignature(
|
|
133
|
+
await createCredential({
|
|
134
|
+
challenge: challenge(),
|
|
135
|
+
config,
|
|
136
|
+
context: {},
|
|
137
|
+
}),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
expect(first.extensions?.mppx?.info.nonce).not.toBe(second.extensions?.mppx?.info.nonce)
|
|
141
|
+
if (!('authorization' in first.payload) || !('authorization' in second.payload))
|
|
142
|
+
throw new Error()
|
|
143
|
+
expect(first.payload.authorization.nonce).not.toBe(second.payload.authorization.nonce)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
function challenge(overrides: Partial<Types.PaymentRequirements> = {}): X402Challenge {
|
|
148
|
+
return Challenge.from({
|
|
149
|
+
id: 'x402-test',
|
|
150
|
+
intent: 'charge',
|
|
151
|
+
method: 'evm',
|
|
152
|
+
realm: 'example.com',
|
|
153
|
+
request: {
|
|
154
|
+
amount: '10000',
|
|
155
|
+
asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
|
|
156
|
+
extra: {
|
|
157
|
+
assetTransferMethod: 'eip3009',
|
|
158
|
+
name: 'USDC',
|
|
159
|
+
version: '2',
|
|
160
|
+
},
|
|
161
|
+
maxTimeoutSeconds: 60,
|
|
162
|
+
network: 'eip155:84532',
|
|
163
|
+
payTo: '0x2222222222222222222222222222222222222222',
|
|
164
|
+
scheme: 'exact',
|
|
165
|
+
extensions: {
|
|
166
|
+
mppx: {
|
|
167
|
+
info: { method: 'GET' },
|
|
168
|
+
schema: {
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
properties: { method: { type: 'string' }, nonce: { type: 'string' } },
|
|
171
|
+
required: ['method'],
|
|
172
|
+
type: 'object',
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
resource: { url: 'https://example.com/paid' },
|
|
177
|
+
...overrides,
|
|
178
|
+
},
|
|
179
|
+
}) as X402Challenge
|
|
180
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { Account } from 'viem'
|
|
2
|
+
import { getAddress, parseUnits } from 'viem'
|
|
3
|
+
|
|
4
|
+
import type * as Challenge from '../../Challenge.js'
|
|
5
|
+
import * as Assets from '../Assets.js'
|
|
6
|
+
import * as Header from '../Header.js'
|
|
7
|
+
import * as RouteBinding from '../internal/RouteBinding.js'
|
|
8
|
+
import * as Types from '../Types.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates an x402 exact `PAYMENT-SIGNATURE` credential.
|
|
12
|
+
*/
|
|
13
|
+
export async function createCredential(parameters: createCredential.Parameters): Promise<string> {
|
|
14
|
+
const account = (parameters.context?.account ?? parameters.config.account) as Signer
|
|
15
|
+
if (!account.signTypedData) throw new Error('x402 exact requires a typed-data signer.')
|
|
16
|
+
|
|
17
|
+
const request = parameters.challenge.request as Types.ExactRequest
|
|
18
|
+
const accepted = Types.toPaymentRequirements(request)
|
|
19
|
+
assertPolicy(parameters.config, accepted)
|
|
20
|
+
if (!request.resource || !request.extensions?.mppx)
|
|
21
|
+
throw new Error('x402 exact EIP-3009 requires route binding.')
|
|
22
|
+
const extensions = withNonceSalt(request.extensions)
|
|
23
|
+
const transferMethod = accepted.extra?.assetTransferMethod ?? 'eip3009'
|
|
24
|
+
if (transferMethod !== 'eip3009')
|
|
25
|
+
throw new Error(`x402 exact ${String(transferMethod)} signing is not implemented yet.`)
|
|
26
|
+
|
|
27
|
+
const name = accepted.extra?.name
|
|
28
|
+
const version = accepted.extra?.version
|
|
29
|
+
if (typeof name !== 'string' || typeof version !== 'string')
|
|
30
|
+
throw new Error('x402 exact EIP-3009 requires token name and version.')
|
|
31
|
+
|
|
32
|
+
const now = Math.floor(Date.now() / 1000)
|
|
33
|
+
const authorization: Types.ExactEip3009Payload['authorization'] = {
|
|
34
|
+
from: getAddress(account.address),
|
|
35
|
+
nonce: RouteBinding.nonce({
|
|
36
|
+
accepted,
|
|
37
|
+
extensions,
|
|
38
|
+
resource: request.resource,
|
|
39
|
+
}),
|
|
40
|
+
to: getAddress(accepted.payTo),
|
|
41
|
+
validAfter: (now - 600).toString(),
|
|
42
|
+
validBefore: (now + accepted.maxTimeoutSeconds).toString(),
|
|
43
|
+
value: accepted.amount,
|
|
44
|
+
}
|
|
45
|
+
const signature = await account.signTypedData({
|
|
46
|
+
domain: {
|
|
47
|
+
chainId: chainIdOf(accepted.network),
|
|
48
|
+
name,
|
|
49
|
+
verifyingContract: getAddress(accepted.asset),
|
|
50
|
+
version,
|
|
51
|
+
},
|
|
52
|
+
message: {
|
|
53
|
+
...authorization,
|
|
54
|
+
value: BigInt(authorization.value),
|
|
55
|
+
validAfter: BigInt(authorization.validAfter),
|
|
56
|
+
validBefore: BigInt(authorization.validBefore),
|
|
57
|
+
},
|
|
58
|
+
primaryType: 'TransferWithAuthorization',
|
|
59
|
+
types: authorizationTypes,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return Header.encodePaymentSignature({
|
|
63
|
+
accepted,
|
|
64
|
+
extensions,
|
|
65
|
+
payload: {
|
|
66
|
+
authorization,
|
|
67
|
+
signature,
|
|
68
|
+
},
|
|
69
|
+
...(request.resource ? { resource: request.resource } : {}),
|
|
70
|
+
x402Version: 2,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export declare namespace createCredential {
|
|
75
|
+
type Parameters = {
|
|
76
|
+
challenge: Challenge.Challenge<Types.ExactRequest>
|
|
77
|
+
config: Config
|
|
78
|
+
context?: Context | undefined
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type Context = {
|
|
83
|
+
account?: Account | undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type Signer = Account & {
|
|
87
|
+
signTypedData?: (parameters: any) => Promise<`0x${string}`>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type Config = {
|
|
91
|
+
/** Account used to sign exact EVM payment payloads. */
|
|
92
|
+
account: Account
|
|
93
|
+
/** Optional token decimals used to parse `maxAmount` when currency metadata is not provided. */
|
|
94
|
+
decimals?: number | undefined
|
|
95
|
+
/** Optional maximum display-unit amount the client is willing to pay. */
|
|
96
|
+
maxAmount?: string | undefined
|
|
97
|
+
/** Optional maximum atomic amount the client is willing to pay. */
|
|
98
|
+
maxAtomicAmount?: string | undefined
|
|
99
|
+
/** Optional allowlist of supported EVM chain IDs. */
|
|
100
|
+
networks?: readonly number[] | undefined
|
|
101
|
+
/** Optional allowlist of supported currencies. */
|
|
102
|
+
currencies?: readonly (`0x${string}` | Assets.KnownAsset)[] | undefined
|
|
103
|
+
/** Legacy alias for `currencies`. */
|
|
104
|
+
assets?: readonly (`0x${string}` | Assets.KnownAsset)[] | undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const authorizationTypes = {
|
|
108
|
+
TransferWithAuthorization: [
|
|
109
|
+
{ name: 'from', type: 'address' },
|
|
110
|
+
{ name: 'to', type: 'address' },
|
|
111
|
+
{ name: 'value', type: 'uint256' },
|
|
112
|
+
{ name: 'validAfter', type: 'uint256' },
|
|
113
|
+
{ name: 'validBefore', type: 'uint256' },
|
|
114
|
+
{ name: 'nonce', type: 'bytes32' },
|
|
115
|
+
],
|
|
116
|
+
} as const
|
|
117
|
+
|
|
118
|
+
function chainIdOf(network: Types.EvmNetwork): number {
|
|
119
|
+
return Number(network.slice(Types.evmNetworkPrefix.length))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assertPolicy(parameters: Config, accepted: Types.PaymentRequirements) {
|
|
123
|
+
const chainId = chainIdOf(accepted.network)
|
|
124
|
+
if (parameters.networks && !parameters.networks.includes(chainId))
|
|
125
|
+
throw new Error(`x402 exact chain ID is not allowed: ${chainId}.`)
|
|
126
|
+
|
|
127
|
+
const currencies = parameters.currencies ?? parameters.assets
|
|
128
|
+
if (currencies?.some((currency) => !Assets.isAsset(currency)) && !parameters.networks?.length)
|
|
129
|
+
throw new Error('x402 exact raw currency allowlists require networks.')
|
|
130
|
+
if (currencies) {
|
|
131
|
+
const acceptedCurrency = getAddress(accepted.asset as `0x${string}`)
|
|
132
|
+
const allowed = currencies.some((currency) =>
|
|
133
|
+
currencyMatches(currency, acceptedCurrency, accepted.network),
|
|
134
|
+
)
|
|
135
|
+
if (!allowed) throw new Error(`x402 exact currency is not allowed: ${acceptedCurrency}.`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
parameters.maxAtomicAmount !== undefined &&
|
|
140
|
+
BigInt(accepted.amount) > BigInt(parameters.maxAtomicAmount)
|
|
141
|
+
)
|
|
142
|
+
throw new Error('x402 exact amount exceeds maxAtomicAmount.')
|
|
143
|
+
|
|
144
|
+
if (parameters.maxAmount !== undefined) {
|
|
145
|
+
const decimals = decimalsOfAcceptedCurrency(parameters, accepted)
|
|
146
|
+
if (decimals === undefined) throw new Error('x402 exact maxAmount requires currency decimals.')
|
|
147
|
+
if (BigInt(accepted.amount) > parseUnits(parameters.maxAmount, decimals))
|
|
148
|
+
throw new Error('x402 exact amount exceeds maxAmount.')
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function addressOf(currency: `0x${string}` | Assets.KnownAsset): `0x${string}` {
|
|
153
|
+
return Assets.isAsset(currency) ? currency.address : currency
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function currencyMatches(
|
|
157
|
+
currency: `0x${string}` | Assets.KnownAsset,
|
|
158
|
+
acceptedCurrency: `0x${string}`,
|
|
159
|
+
network: Types.EvmNetwork,
|
|
160
|
+
): boolean {
|
|
161
|
+
if (getAddress(addressOf(currency)) !== acceptedCurrency) return false
|
|
162
|
+
return !Assets.isAsset(currency) || currency.network === network
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function decimalsOfAcceptedCurrency(
|
|
166
|
+
parameters: Config,
|
|
167
|
+
accepted: Types.PaymentRequirements,
|
|
168
|
+
): number | undefined {
|
|
169
|
+
const currencies = parameters.currencies ?? parameters.assets
|
|
170
|
+
const acceptedCurrency = getAddress(accepted.asset as `0x${string}`)
|
|
171
|
+
const currency = currencies?.find((currency) =>
|
|
172
|
+
currencyMatches(currency, acceptedCurrency, accepted.network),
|
|
173
|
+
)
|
|
174
|
+
if (currency && Assets.isAsset(currency)) return currency.decimals
|
|
175
|
+
return parameters.decimals
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function withNonceSalt(extensions: Types.Extensions): Types.Extensions {
|
|
179
|
+
const mppx = extensions.mppx
|
|
180
|
+
if (!mppx) return extensions
|
|
181
|
+
return {
|
|
182
|
+
...extensions,
|
|
183
|
+
mppx: {
|
|
184
|
+
...mppx,
|
|
185
|
+
info: {
|
|
186
|
+
...mppx.info,
|
|
187
|
+
nonce: randomNonceSalt(),
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function randomNonceSalt(): string {
|
|
194
|
+
const crypto = globalThis.crypto
|
|
195
|
+
if (!crypto?.getRandomValues) throw new Error('x402 exact requires crypto randomness.')
|
|
196
|
+
const bytes = crypto.getRandomValues(new Uint8Array(32))
|
|
197
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
198
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const x402Challenge = Symbol.for('mppx.x402.challenge')
|
|
2
|
+
|
|
3
|
+
/** Marks a synthetic challenge as originating from an x402 `PAYMENT-REQUIRED` response. */
|
|
4
|
+
export function mark<const challenge extends object>(challenge: challenge): challenge {
|
|
5
|
+
Object.defineProperty(challenge, x402Challenge, {
|
|
6
|
+
value: true,
|
|
7
|
+
})
|
|
8
|
+
return challenge
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Returns whether a challenge originated from an x402 `PAYMENT-REQUIRED` response. */
|
|
12
|
+
export function is(challenge: unknown): boolean {
|
|
13
|
+
return Boolean((challenge as { [x402Challenge]?: true } | undefined)?.[x402Challenge])
|
|
14
|
+
}
|