mppx 0.4.6 → 0.4.8
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 +12 -0
- package/dist/Store.d.ts +5 -4
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +22 -7
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +9 -22
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +5 -1
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +3 -1
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts +2 -2
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +4 -2
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +26 -8
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +12 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/address.d.ts +3 -0
- package/dist/tempo/internal/address.d.ts.map +1 -0
- package/dist/tempo/internal/address.js +4 -0
- package/dist/tempo/internal/address.js.map +1 -0
- package/dist/tempo/internal/auto-swap.js +3 -3
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +4 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +11 -3
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +11 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +109 -50
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +39 -32
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +41 -1
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +51 -10
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +2 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +4 -2
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js +3 -2
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/package.json +6 -2
- package/src/Store.test-d.ts +58 -0
- package/src/Store.ts +6 -4
- package/src/cli/cli.test.ts +124 -0
- package/src/cli/cli.ts +19 -7
- package/src/cli/plugins/tempo.ts +17 -23
- package/src/middlewares/elysia.test.ts +89 -0
- package/src/middlewares/elysia.ts +4 -1
- package/src/proxy/Proxy.test.ts +56 -0
- package/src/proxy/Proxy.ts +6 -1
- package/src/proxy/internal/Route.test.ts +57 -0
- package/src/proxy/internal/Route.ts +3 -1
- package/src/server/Mppx.test.ts +246 -0
- package/src/server/Mppx.ts +27 -8
- package/src/tempo/client/SessionManager.ts +11 -1
- package/src/tempo/internal/address.ts +6 -0
- package/src/tempo/internal/auto-swap.ts +3 -3
- package/src/tempo/internal/fee-payer.ts +18 -4
- package/src/tempo/server/Charge.test.ts +1080 -31
- package/src/tempo/server/Charge.ts +158 -63
- package/src/tempo/server/Session.test.ts +929 -111
- package/src/tempo/server/Session.ts +48 -33
- package/src/tempo/server/Sse.test.ts +1 -0
- package/src/tempo/server/internal/transport.test.ts +29 -0
- package/src/tempo/server/internal/transport.ts +41 -2
- package/src/tempo/session/Chain.test.ts +144 -0
- package/src/tempo/session/Chain.ts +58 -10
- package/src/tempo/session/ChannelStore.test.ts +10 -0
- package/src/tempo/session/ChannelStore.ts +6 -3
- package/src/tempo/session/Sse.test.ts +1 -0
- package/src/tempo/session/Voucher.ts +3 -2
package/src/server/Mppx.ts
CHANGED
|
@@ -329,19 +329,36 @@ function createMethodFn(parameters: createMethodFn.Parameters): createMethodFn.R
|
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
// Verify the credential's challenge matches this route's configured
|
|
332
|
-
// request. Prevents cross-route scope
|
|
333
|
-
// issued for a cheap route
|
|
332
|
+
// method, intent, realm, and request. Prevents cross-route scope
|
|
333
|
+
// confusion where a credential issued for a cheap route (or different
|
|
334
|
+
// method/intent) is presented at an expensive route.
|
|
334
335
|
// Note: we compare specific payment parameters rather than the full
|
|
335
336
|
// request because the `request` hook may produce credential-dependent
|
|
336
337
|
// output (e.g. `feePayer` differs between 402 and credential calls).
|
|
337
338
|
{
|
|
339
|
+
for (const field of ['method', 'intent', 'realm'] as const) {
|
|
340
|
+
if (credential.challenge[field] !== challenge[field]) {
|
|
341
|
+
const response = await transport.respondChallenge({
|
|
342
|
+
challenge,
|
|
343
|
+
input,
|
|
344
|
+
error: new Errors.InvalidChallengeError({
|
|
345
|
+
id: credential.challenge.id,
|
|
346
|
+
reason: `credential ${field} does not match this route's requirements`,
|
|
347
|
+
}),
|
|
348
|
+
})
|
|
349
|
+
return { challenge: response, status: 402 }
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
338
353
|
const routeReq = challenge.request as Record<string, unknown>
|
|
339
354
|
const echoedReq = credential.challenge.request as Record<string, unknown>
|
|
355
|
+
const routeDetails = (routeReq.methodDetails ?? {}) as Record<string, unknown>
|
|
356
|
+
const echoedDetails = (echoedReq.methodDetails ?? {}) as Record<string, unknown>
|
|
340
357
|
for (const field of ['amount', 'currency', 'recipient'] as const) {
|
|
358
|
+
const routeVal = routeReq[field] ?? routeDetails[field]
|
|
341
359
|
if (
|
|
342
|
-
|
|
343
|
-
echoedReq[field]
|
|
344
|
-
String(routeReq[field]) !== String(echoedReq[field])
|
|
360
|
+
routeVal !== undefined &&
|
|
361
|
+
String(routeVal) !== String(echoedReq[field] ?? echoedDetails[field])
|
|
345
362
|
) {
|
|
346
363
|
const response = await transport.respondChallenge({
|
|
347
364
|
challenge,
|
|
@@ -578,22 +595,24 @@ export function compose(
|
|
|
578
595
|
if (credential) {
|
|
579
596
|
const { method: credMethod, intent: credIntent } = credential.challenge
|
|
580
597
|
const credReq = credential.challenge.request as Record<string, unknown>
|
|
598
|
+
const credDetails = (credReq.methodDetails ?? {}) as Record<string, unknown>
|
|
581
599
|
|
|
582
600
|
// Filter by name+intent, then narrow by comparing stable request fields
|
|
583
601
|
// from the echoed challenge against each handler's canonical request.
|
|
584
602
|
// Uses the schema-parsed canonical form (not raw options) so that
|
|
585
603
|
// transformed fields (e.g. amount with decimals) match correctly.
|
|
604
|
+
// Also checks inside methodDetails for fields moved there by transforms.
|
|
586
605
|
const candidates = handlers.filter((h) => {
|
|
587
606
|
const meta = (h as ConfiguredHandler)._internal
|
|
588
607
|
if (!meta || meta.name !== credMethod || meta.intent !== credIntent) return false
|
|
589
608
|
const canonical = meta._canonicalRequest
|
|
590
609
|
if (!canonical) return true
|
|
610
|
+
const canonicalDetails = (canonical.methodDetails ?? {}) as Record<string, unknown>
|
|
591
611
|
for (const field of ['amount', 'currency', 'recipient', 'chainId'] as const) {
|
|
592
|
-
const canonicalVal = canonical[field]
|
|
612
|
+
const canonicalVal = canonical[field] ?? canonicalDetails[field]
|
|
593
613
|
if (
|
|
594
614
|
canonicalVal !== undefined &&
|
|
595
|
-
credReq[field]
|
|
596
|
-
String(canonicalVal) !== String(credReq[field])
|
|
615
|
+
String(canonicalVal) !== String(credReq[field] ?? credDetails[field])
|
|
597
616
|
)
|
|
598
617
|
return false
|
|
599
618
|
}
|
|
@@ -59,6 +59,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
59
59
|
let channel: ChannelEntry | null = null
|
|
60
60
|
let lastChallenge: Challenge.Challenge | null = null
|
|
61
61
|
let lastUrl: RequestInfo | URL | null = null
|
|
62
|
+
let spent = 0n
|
|
62
63
|
|
|
63
64
|
const method = sessionPlugin({
|
|
64
65
|
account: parameters.account,
|
|
@@ -68,6 +69,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
68
69
|
decimals: parameters.decimals,
|
|
69
70
|
maxDeposit: parameters.maxDeposit,
|
|
70
71
|
onChannelUpdate(entry) {
|
|
72
|
+
if (entry.channelId !== channel?.channelId) spent = 0n
|
|
71
73
|
channel = entry
|
|
72
74
|
},
|
|
73
75
|
})
|
|
@@ -81,9 +83,16 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
81
83
|
},
|
|
82
84
|
})
|
|
83
85
|
|
|
86
|
+
function updateSpentFromReceipt(receipt: SessionReceipt | null | undefined) {
|
|
87
|
+
if (!receipt || receipt.channelId !== channel?.channelId) return
|
|
88
|
+
const next = BigInt(receipt.spent)
|
|
89
|
+
spent = spent > next ? spent : next
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
function toPaymentResponse(response: Response): PaymentResponse {
|
|
85
93
|
const receiptHeader = response.headers.get('Payment-Receipt')
|
|
86
94
|
const receipt = receiptHeader ? deserializeSessionReceipt(receiptHeader) : null
|
|
95
|
+
updateSpentFromReceipt(receipt)
|
|
87
96
|
return Object.assign(response, {
|
|
88
97
|
receipt,
|
|
89
98
|
challenge: lastChallenge,
|
|
@@ -216,6 +225,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
216
225
|
}
|
|
217
226
|
|
|
218
227
|
case 'payment-receipt':
|
|
228
|
+
updateSpentFromReceipt(event.data)
|
|
219
229
|
onReceipt?.(event.data)
|
|
220
230
|
break
|
|
221
231
|
}
|
|
@@ -237,7 +247,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
237
247
|
context: {
|
|
238
248
|
action: 'close',
|
|
239
249
|
channelId: channel.channelId,
|
|
240
|
-
cumulativeAmountRaw:
|
|
250
|
+
cumulativeAmountRaw: spent.toString(),
|
|
241
251
|
},
|
|
242
252
|
})
|
|
243
253
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Address, Client } from 'viem'
|
|
2
|
-
import { isAddressEqual } from 'viem'
|
|
3
2
|
import { readContract } from 'viem/actions'
|
|
4
3
|
import { Actions, Addresses } from 'viem/tempo'
|
|
4
|
+
import * as TempoAddress from './address.js'
|
|
5
5
|
import * as defaults from './defaults.js'
|
|
6
6
|
|
|
7
7
|
/** Basis-point denominator (100% = 10 000 bps). */
|
|
@@ -26,7 +26,7 @@ export async function findCalls(
|
|
|
26
26
|
): Promise<findCalls.ReturnType> {
|
|
27
27
|
const { account, amountOut, tokenOut, tokenIn, slippage } = parameters
|
|
28
28
|
|
|
29
|
-
const candidates = tokenIn.filter((t) => !
|
|
29
|
+
const candidates = tokenIn.filter((t) => !TempoAddress.isEqual(t, tokenOut))
|
|
30
30
|
|
|
31
31
|
const balanceResults = await Promise.allSettled([
|
|
32
32
|
readContract(client, Actions.token.getBalance.call({ account, token: tokenOut }) as never),
|
|
@@ -108,7 +108,7 @@ export function resolve(
|
|
|
108
108
|
const tokenIn = value.tokenIn
|
|
109
109
|
? [
|
|
110
110
|
...value.tokenIn,
|
|
111
|
-
...defaultCurrencies.filter((d) => !value.tokenIn!.some((c) =>
|
|
111
|
+
...defaultCurrencies.filter((d) => !value.tokenIn!.some((c) => TempoAddress.isEqual(c, d))),
|
|
112
112
|
]
|
|
113
113
|
: defaultCurrencies
|
|
114
114
|
return {
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TempoAddress } from 'ox/tempo'
|
|
2
|
+
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
3
|
+
import { decodeFunctionData } from 'viem'
|
|
2
4
|
import { Abis, Addresses } from 'viem/tempo'
|
|
5
|
+
import * as TempoAddress_internal from './address.js'
|
|
3
6
|
import * as Selectors from './selectors.js'
|
|
4
7
|
|
|
8
|
+
/** Returns true if the serialized transaction has a Tempo envelope prefix. */
|
|
9
|
+
export function isTempoTransaction(serialized: string | undefined): boolean {
|
|
10
|
+
return (
|
|
11
|
+
serialized?.startsWith(TxEnvelopeTempo.serializedType) === true ||
|
|
12
|
+
serialized?.startsWith(TxEnvelopeTempo.feePayerMagic) === true
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
/**
|
|
6
17
|
* Allowed call patterns for fee-payer sponsored transactions.
|
|
7
18
|
* Each inner array is an ordered list of function selectors.
|
|
@@ -15,7 +26,7 @@ export const callScopes = [
|
|
|
15
26
|
|
|
16
27
|
/** Validates that a set of transaction calls matches an allowed fee-payer pattern. */
|
|
17
28
|
export function validateCalls(
|
|
18
|
-
calls: readonly { data?: `0x${string}` | undefined; to?:
|
|
29
|
+
calls: readonly { data?: `0x${string}` | undefined; to?: TempoAddress.Address | undefined }[],
|
|
19
30
|
details: Record<string, string>,
|
|
20
31
|
) {
|
|
21
32
|
const callSelectors = calls.map((c) => c.data?.slice(0, 10))
|
|
@@ -31,11 +42,14 @@ export function validateCalls(
|
|
|
31
42
|
const approveCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.approve)
|
|
32
43
|
if (approveCall) {
|
|
33
44
|
const { args } = decodeFunctionData({ abi: Abis.tip20, data: approveCall.data! })
|
|
34
|
-
if (!
|
|
45
|
+
if (!TempoAddress_internal.isEqual((args as [`0x${string}`])[0]!, Addresses.stablecoinDex))
|
|
35
46
|
throw new FeePayerValidationError('approve spender is not the DEX', details)
|
|
36
47
|
}
|
|
37
48
|
const buyCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.swapExactAmountOut)
|
|
38
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
buyCall &&
|
|
51
|
+
(!buyCall.to || !TempoAddress_internal.isEqual(buyCall.to, Addresses.stablecoinDex))
|
|
52
|
+
)
|
|
39
53
|
throw new FeePayerValidationError('buy target is not the DEX', details)
|
|
40
54
|
}
|
|
41
55
|
|