mppx 0.8.4 → 0.8.6
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 +18 -0
- package/dist/Receipt.d.ts +20 -2
- package/dist/Receipt.d.ts.map +1 -1
- package/dist/Receipt.js +19 -12
- package/dist/Receipt.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +6 -4
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +4 -2
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +6 -1
- package/dist/client/internal/Fetch.js.map +1 -1
- 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/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +6 -2
- package/dist/tempo/client/Charge.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/precompile/Chain.d.ts +26 -2
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +38 -5
- package/dist/tempo/session/precompile/Chain.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/Receipt.test.ts +53 -0
- package/src/Receipt.ts +26 -13
- package/src/cli/cli.test.ts +41 -5
- package/src/cli/cli.ts +7 -4
- package/src/cli/plugins/tempo.ts +6 -2
- package/src/client/internal/Fetch.test.ts +44 -0
- package/src/client/internal/Fetch.ts +5 -1
- 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/client/Charge.test.ts +61 -0
- package/src/tempo/client/Charge.ts +6 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/precompile/Chain.test.ts +121 -2
- package/src/tempo/session/precompile/Chain.ts +56 -6
- 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
|
@@ -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.')
|
|
@@ -353,6 +353,67 @@ describe('tempo.charge client', () => {
|
|
|
353
353
|
}
|
|
354
354
|
})
|
|
355
355
|
|
|
356
|
+
test('normalizes sponsored pull transactions before signing', async () => {
|
|
357
|
+
vi.resetModules()
|
|
358
|
+
const prepared = {
|
|
359
|
+
feePayerSignature: { r: '0x1', s: '0x2', yParity: 0 },
|
|
360
|
+
feeToken: currency,
|
|
361
|
+
gas: 100n,
|
|
362
|
+
}
|
|
363
|
+
const prepareTransactionRequest = vi.fn(async () => prepared)
|
|
364
|
+
const signTransaction = vi.fn(
|
|
365
|
+
async (_client: unknown, _transaction: Record<string, unknown>) => '0xdeadbeef',
|
|
366
|
+
)
|
|
367
|
+
vi.doMock('viem/actions', () => ({
|
|
368
|
+
prepareTransactionRequest,
|
|
369
|
+
sendCallsSync: vi.fn(),
|
|
370
|
+
signTransaction,
|
|
371
|
+
signTypedData: vi.fn(),
|
|
372
|
+
}))
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const { charge: chargeWithMockedActions } = await import('./Charge.js')
|
|
376
|
+
const chainId = 42431
|
|
377
|
+
const client = createClient({
|
|
378
|
+
account,
|
|
379
|
+
chain: tempoLocalnet,
|
|
380
|
+
transport: http('http://127.0.0.1'),
|
|
381
|
+
})
|
|
382
|
+
const method = chargeWithMockedActions({
|
|
383
|
+
account,
|
|
384
|
+
getClient: () => client,
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
await method.createCredential({
|
|
388
|
+
challenge: createChallenge({
|
|
389
|
+
amount: '1',
|
|
390
|
+
chainId,
|
|
391
|
+
feePayer: true,
|
|
392
|
+
supportedModes: ['pull'],
|
|
393
|
+
}),
|
|
394
|
+
context: {},
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
expect(prepareTransactionRequest).toHaveBeenCalledWith(
|
|
398
|
+
client,
|
|
399
|
+
expect.not.objectContaining({ feePayer: true }),
|
|
400
|
+
)
|
|
401
|
+
expect(signTransaction).toHaveBeenCalledWith(
|
|
402
|
+
client,
|
|
403
|
+
expect.objectContaining({
|
|
404
|
+
feePayer: true,
|
|
405
|
+
gas: 5_100n,
|
|
406
|
+
}),
|
|
407
|
+
)
|
|
408
|
+
const signed = signTransaction.mock.calls[0]?.[1] as Record<string, unknown>
|
|
409
|
+
expect(signed).not.toHaveProperty('feePayerSignature')
|
|
410
|
+
expect(signed).not.toHaveProperty('feeToken')
|
|
411
|
+
} finally {
|
|
412
|
+
vi.doUnmock('viem/actions')
|
|
413
|
+
vi.resetModules()
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
356
417
|
describe('chain pinning', () => {
|
|
357
418
|
const client = createClient({
|
|
358
419
|
account,
|
|
@@ -226,7 +226,12 @@ export function charge(parameters: charge.Parameters = {}) {
|
|
|
226
226
|
// Estimate before enabling fee-payer mode so Tempo includes sender
|
|
227
227
|
// signature and access-key verification costs in the gas budget.
|
|
228
228
|
prepared.gas = (prepared.gas ?? 0n) + 5_000n
|
|
229
|
-
if (methodDetails?.feePayer)
|
|
229
|
+
if (methodDetails?.feePayer) {
|
|
230
|
+
const sponsored = prepared as Record<string, unknown>
|
|
231
|
+
delete sponsored.feePayerSignature
|
|
232
|
+
delete sponsored.feeToken
|
|
233
|
+
sponsored.feePayer = true
|
|
234
|
+
}
|
|
230
235
|
const signature = await signTransaction(client, prepared as never)
|
|
231
236
|
|
|
232
237
|
return Credential.serialize({
|