mppx 0.3.2 → 0.3.4
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/dist/Errors.d.ts +7 -7
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +7 -7
- package/dist/Errors.js.map +1 -1
- package/dist/cli.js +98 -64
- package/dist/cli.js.map +1 -1
- package/dist/internal/env.d.ts +19 -0
- package/dist/internal/env.d.ts.map +1 -0
- package/dist/internal/env.js +55 -0
- package/dist/internal/env.js.map +1 -0
- package/dist/server/Mppx.d.ts +2 -2
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +2 -1
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/client/ChannelOps.d.ts +5 -5
- package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/client/ChannelOps.js +3 -3
- package/dist/tempo/client/ChannelOps.js.map +1 -1
- package/dist/tempo/client/Session.d.ts +2 -2
- package/dist/tempo/client/Session.d.ts.map +1 -1
- package/dist/tempo/client/Session.js +3 -3
- package/dist/tempo/client/Session.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +4 -4
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +4 -4
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/index.d.ts +1 -1
- package/dist/tempo/index.d.ts.map +1 -1
- package/dist/tempo/index.js +1 -1
- package/dist/tempo/index.js.map +1 -1
- package/dist/tempo/server/Charge.js +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +1 -1
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Session.d.ts +8 -8
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +24 -24
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/index.d.ts +2 -2
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js +2 -2
- package/dist/tempo/server/index.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +4 -4
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +3 -3
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -0
- package/dist/tempo/session/Chain.js.map +1 -0
- package/dist/tempo/session/Channel.d.ts.map +1 -0
- package/dist/tempo/session/Channel.js.map +1 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -0
- package/dist/tempo/session/ChannelStore.js.map +1 -0
- package/dist/tempo/session/Receipt.d.ts +22 -0
- package/dist/tempo/session/Receipt.d.ts.map +1 -0
- package/dist/tempo/{stream → session}/Receipt.js +6 -6
- package/dist/tempo/session/Receipt.js.map +1 -0
- package/dist/tempo/{stream → session}/Sse.d.ts +7 -7
- package/dist/tempo/session/Sse.d.ts.map +1 -0
- package/dist/tempo/{stream → session}/Sse.js +4 -4
- package/dist/tempo/session/Sse.js.map +1 -0
- package/dist/tempo/{stream → session}/Types.d.ts +4 -4
- package/dist/tempo/session/Types.d.ts.map +1 -0
- package/dist/tempo/{stream → session}/Types.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -0
- package/dist/tempo/session/Voucher.js.map +1 -0
- package/dist/tempo/{stream → session}/escrow.abi.d.ts.map +1 -1
- package/dist/tempo/session/escrow.abi.js.map +1 -0
- package/dist/tempo/session/index.d.ts.map +1 -0
- package/dist/tempo/session/index.js.map +1 -0
- package/package.json +1 -1
- package/src/Errors.test.ts +10 -10
- package/src/Errors.ts +7 -7
- package/src/Expires.test.ts +111 -0
- package/src/cli.test.ts +3 -3
- package/src/cli.ts +125 -70
- package/src/internal/env.ts +54 -0
- package/src/mcp-sdk/server/Transport.test.ts +171 -0
- package/src/middlewares/express.test.ts +1 -1
- package/src/middlewares/hono.test.ts +1 -1
- package/src/middlewares/nextjs.test.ts +1 -1
- package/src/server/Mppx.ts +5 -4
- package/src/server/Transport.test.ts +1 -1
- package/src/tempo/client/ChannelOps.test.ts +290 -0
- package/src/tempo/client/ChannelOps.ts +8 -8
- package/src/tempo/client/Session.test.ts +467 -0
- package/src/tempo/client/Session.ts +9 -9
- package/src/tempo/client/SessionManager.test.ts +3 -3
- package/src/tempo/client/SessionManager.ts +9 -9
- package/src/tempo/index.ts +1 -1
- package/src/tempo/server/Charge.ts +1 -1
- package/src/tempo/server/Session.test.ts +9 -9
- package/src/tempo/server/Session.ts +47 -47
- package/src/tempo/server/Sse.test.ts +3 -3
- package/src/tempo/server/index.ts +2 -2
- package/src/tempo/server/internal/transport.ts +6 -6
- package/src/tempo/session/Chain.test.ts +511 -0
- package/src/tempo/session/Channel.test.ts +108 -0
- package/src/tempo/{stream → session}/Receipt.test.ts +16 -12
- package/src/tempo/{stream → session}/Receipt.ts +9 -9
- package/src/tempo/{stream → session}/Sse.test.ts +5 -5
- package/src/tempo/{stream → session}/Sse.ts +11 -11
- package/src/tempo/{stream → session}/Types.ts +4 -4
- package/dist/tempo/stream/Chain.d.ts.map +0 -1
- package/dist/tempo/stream/Chain.js.map +0 -1
- package/dist/tempo/stream/Channel.d.ts.map +0 -1
- package/dist/tempo/stream/Channel.js.map +0 -1
- package/dist/tempo/stream/ChannelStore.d.ts.map +0 -1
- package/dist/tempo/stream/ChannelStore.js.map +0 -1
- package/dist/tempo/stream/Receipt.d.ts +0 -22
- package/dist/tempo/stream/Receipt.d.ts.map +0 -1
- package/dist/tempo/stream/Receipt.js.map +0 -1
- package/dist/tempo/stream/Sse.d.ts.map +0 -1
- package/dist/tempo/stream/Sse.js.map +0 -1
- package/dist/tempo/stream/Types.d.ts.map +0 -1
- package/dist/tempo/stream/Voucher.d.ts.map +0 -1
- package/dist/tempo/stream/Voucher.js.map +0 -1
- package/dist/tempo/stream/escrow.abi.js.map +0 -1
- package/dist/tempo/stream/index.d.ts.map +0 -1
- package/dist/tempo/stream/index.js.map +0 -1
- /package/dist/tempo/{stream → session}/Chain.d.ts +0 -0
- /package/dist/tempo/{stream → session}/Chain.js +0 -0
- /package/dist/tempo/{stream → session}/Channel.d.ts +0 -0
- /package/dist/tempo/{stream → session}/Channel.js +0 -0
- /package/dist/tempo/{stream → session}/ChannelStore.d.ts +0 -0
- /package/dist/tempo/{stream → session}/ChannelStore.js +0 -0
- /package/dist/tempo/{stream → session}/Types.js +0 -0
- /package/dist/tempo/{stream → session}/Voucher.d.ts +0 -0
- /package/dist/tempo/{stream → session}/Voucher.js +0 -0
- /package/dist/tempo/{stream → session}/escrow.abi.d.ts +0 -0
- /package/dist/tempo/{stream → session}/escrow.abi.js +0 -0
- /package/dist/tempo/{stream → session}/index.d.ts +0 -0
- /package/dist/tempo/{stream → session}/index.js +0 -0
- /package/src/tempo/{stream → session}/Chain.ts +0 -0
- /package/src/tempo/{stream → session}/Channel.ts +0 -0
- /package/src/tempo/{stream → session}/ChannelStore.test.ts +0 -0
- /package/src/tempo/{stream → session}/ChannelStore.ts +0 -0
- /package/src/tempo/{stream → session}/Voucher.test.ts +0 -0
- /package/src/tempo/{stream → session}/Voucher.ts +0 -0
- /package/src/tempo/{stream → session}/escrow.abi.ts +0 -0
- /package/src/tempo/{stream → session}/index.ts +0 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { type Address, createClient, type Hex, http } from 'viem'
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts'
|
|
3
|
+
import { Addresses } from 'viem/tempo'
|
|
4
|
+
import { beforeAll, describe, expect, test } from 'vitest'
|
|
5
|
+
import { deployEscrow, openChannel } from '~test/tempo/session.js'
|
|
6
|
+
import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
|
|
7
|
+
import * as Challenge from '../../Challenge.js'
|
|
8
|
+
import * as Credential from '../../Credential.js'
|
|
9
|
+
import type { SessionCredentialPayload } from '../session/Types.js'
|
|
10
|
+
import { session } from './Session.js'
|
|
11
|
+
|
|
12
|
+
function deserializePayload(result: string) {
|
|
13
|
+
const cred = Credential.deserialize<SessionCredentialPayload>(result)
|
|
14
|
+
return cred
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const pureAccount = privateKeyToAccount(
|
|
18
|
+
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
|
|
19
|
+
)
|
|
20
|
+
const pureClient = createClient({
|
|
21
|
+
account: pureAccount,
|
|
22
|
+
transport: http('http://127.0.0.1'),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const escrowAddress = '0x542831e3E4Ace07559b7C8787395f4Fb99F70787' as Address
|
|
26
|
+
const recipient = '0x2222222222222222222222222222222222222222' as Address
|
|
27
|
+
const currency = '0x3333333333333333333333333333333333333333' as Address
|
|
28
|
+
|
|
29
|
+
function makeChallenge(overrides?: Record<string, unknown>) {
|
|
30
|
+
return Challenge.from({
|
|
31
|
+
id: 'test-challenge-id',
|
|
32
|
+
realm: 'test.com',
|
|
33
|
+
method: 'tempo',
|
|
34
|
+
intent: 'session',
|
|
35
|
+
request: {
|
|
36
|
+
amount: '1000000',
|
|
37
|
+
recipient,
|
|
38
|
+
currency,
|
|
39
|
+
unitType: 'token',
|
|
40
|
+
methodDetails: {
|
|
41
|
+
chainId: 42431,
|
|
42
|
+
escrowContract: escrowAddress,
|
|
43
|
+
},
|
|
44
|
+
...overrides,
|
|
45
|
+
},
|
|
46
|
+
}) as any
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('session (pure)', () => {
|
|
50
|
+
describe('error: no action and no deposit/maxDeposit', () => {
|
|
51
|
+
test('throws when neither configured', async () => {
|
|
52
|
+
const method = session({
|
|
53
|
+
getClient: () => pureClient,
|
|
54
|
+
account: pureAccount,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
await expect(
|
|
58
|
+
method.createCredential({ challenge: makeChallenge(), context: {} }),
|
|
59
|
+
).rejects.toThrow('No `action` in context and no `deposit` or `maxDeposit` configured')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('manual action validation', () => {
|
|
64
|
+
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
65
|
+
|
|
66
|
+
test('open requires transaction', async () => {
|
|
67
|
+
const method = session({ getClient: () => pureClient, account: pureAccount })
|
|
68
|
+
|
|
69
|
+
await expect(
|
|
70
|
+
method.createCredential({
|
|
71
|
+
challenge: makeChallenge(),
|
|
72
|
+
context: { action: 'open', channelId, cumulativeAmount: '1' },
|
|
73
|
+
}),
|
|
74
|
+
).rejects.toThrow('transaction required for open action')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('open requires cumulativeAmount', async () => {
|
|
78
|
+
const method = session({ getClient: () => pureClient, account: pureAccount })
|
|
79
|
+
|
|
80
|
+
await expect(
|
|
81
|
+
method.createCredential({
|
|
82
|
+
challenge: makeChallenge(),
|
|
83
|
+
context: { action: 'open', channelId, transaction: '0xabc' },
|
|
84
|
+
}),
|
|
85
|
+
).rejects.toThrow('cumulativeAmount required for open action')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('topUp requires transaction', async () => {
|
|
89
|
+
const method = session({ getClient: () => pureClient, account: pureAccount })
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
method.createCredential({
|
|
93
|
+
challenge: makeChallenge(),
|
|
94
|
+
context: { action: 'topUp', channelId, additionalDeposit: '5' },
|
|
95
|
+
}),
|
|
96
|
+
).rejects.toThrow('transaction required for topUp action')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('topUp requires additionalDeposit', async () => {
|
|
100
|
+
const method = session({ getClient: () => pureClient, account: pureAccount })
|
|
101
|
+
|
|
102
|
+
await expect(
|
|
103
|
+
method.createCredential({
|
|
104
|
+
challenge: makeChallenge(),
|
|
105
|
+
context: { action: 'topUp', channelId, transaction: '0xabc' },
|
|
106
|
+
}),
|
|
107
|
+
).rejects.toThrow('additionalDeposit required for topUp action')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('voucher requires cumulativeAmount', async () => {
|
|
111
|
+
const method = session({ getClient: () => pureClient, account: pureAccount })
|
|
112
|
+
|
|
113
|
+
await expect(
|
|
114
|
+
method.createCredential({
|
|
115
|
+
challenge: makeChallenge(),
|
|
116
|
+
context: { action: 'voucher', channelId },
|
|
117
|
+
}),
|
|
118
|
+
).rejects.toThrow('cumulativeAmount required for voucher action')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('close requires cumulativeAmount', async () => {
|
|
122
|
+
const method = session({ getClient: () => pureClient, account: pureAccount })
|
|
123
|
+
|
|
124
|
+
await expect(
|
|
125
|
+
method.createCredential({
|
|
126
|
+
challenge: makeChallenge(),
|
|
127
|
+
context: { action: 'close', channelId },
|
|
128
|
+
}),
|
|
129
|
+
).rejects.toThrow('cumulativeAmount required for close action')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('manual voucher produces valid credential', async () => {
|
|
133
|
+
const method = session({ getClient: () => pureClient, account: pureAccount })
|
|
134
|
+
|
|
135
|
+
const result = await method.createCredential({
|
|
136
|
+
challenge: makeChallenge(),
|
|
137
|
+
context: { action: 'voucher', channelId, cumulativeAmount: '5' },
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const cred = deserializePayload(result)
|
|
141
|
+
expect(cred.challenge.id).toBe('test-challenge-id')
|
|
142
|
+
expect(cred.challenge.realm).toBe('test.com')
|
|
143
|
+
expect(cred.challenge.method).toBe('tempo')
|
|
144
|
+
expect(cred.challenge.intent).toBe('session')
|
|
145
|
+
expect(cred.payload.action).toBe('voucher')
|
|
146
|
+
expect(cred.payload.channelId).toBe(channelId)
|
|
147
|
+
if (cred.payload.action === 'voucher') {
|
|
148
|
+
expect(cred.payload.cumulativeAmount).toBe('5000000')
|
|
149
|
+
expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
150
|
+
}
|
|
151
|
+
expect(cred.source).toBe(`did:pkh:eip155:42431:${pureAccount.address}`)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('manual open produces valid credential', async () => {
|
|
155
|
+
const method = session({ getClient: () => pureClient, account: pureAccount })
|
|
156
|
+
|
|
157
|
+
const result = await method.createCredential({
|
|
158
|
+
challenge: makeChallenge(),
|
|
159
|
+
context: {
|
|
160
|
+
action: 'open',
|
|
161
|
+
channelId,
|
|
162
|
+
cumulativeAmount: '5',
|
|
163
|
+
transaction: '0xdeadbeef',
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const cred = deserializePayload(result)
|
|
168
|
+
expect(cred.challenge.id).toBe('test-challenge-id')
|
|
169
|
+
expect(cred.payload.action).toBe('open')
|
|
170
|
+
expect(cred.payload.channelId).toBe(channelId)
|
|
171
|
+
if (cred.payload.action === 'open') {
|
|
172
|
+
expect(cred.payload.type).toBe('transaction')
|
|
173
|
+
expect(cred.payload.transaction).toBe('0xdeadbeef')
|
|
174
|
+
expect(cred.payload.cumulativeAmount).toBe('5000000')
|
|
175
|
+
expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
176
|
+
expect(cred.payload.authorizedSigner).toBe(pureAccount.address)
|
|
177
|
+
}
|
|
178
|
+
expect(cred.source).toBe(`did:pkh:eip155:42431:${pureAccount.address}`)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('manual close produces valid credential', async () => {
|
|
182
|
+
const method = session({ getClient: () => pureClient, account: pureAccount })
|
|
183
|
+
|
|
184
|
+
const result = await method.createCredential({
|
|
185
|
+
challenge: makeChallenge(),
|
|
186
|
+
context: { action: 'close', channelId, cumulativeAmount: '5' },
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const cred = deserializePayload(result)
|
|
190
|
+
expect(cred.challenge.id).toBe('test-challenge-id')
|
|
191
|
+
expect(cred.payload.action).toBe('close')
|
|
192
|
+
expect(cred.payload.channelId).toBe(channelId)
|
|
193
|
+
if (cred.payload.action === 'close') {
|
|
194
|
+
expect(cred.payload.cumulativeAmount).toBe('5000000')
|
|
195
|
+
expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
196
|
+
}
|
|
197
|
+
expect(cred.source).toBe(`did:pkh:eip155:42431:${pureAccount.address}`)
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('session (on-chain)', () => {
|
|
203
|
+
const payer = accounts[2]
|
|
204
|
+
const payee = accounts[1].address
|
|
205
|
+
let escrowContract: Address
|
|
206
|
+
let saltCounter = 0
|
|
207
|
+
|
|
208
|
+
function nextSalt(): Hex {
|
|
209
|
+
saltCounter++
|
|
210
|
+
return `0x${saltCounter.toString(16).padStart(64, '0')}` as Hex
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function makeLiveChallenge(overrides?: Record<string, unknown>) {
|
|
214
|
+
return Challenge.from({
|
|
215
|
+
id: 'live-challenge',
|
|
216
|
+
realm: 'test.com',
|
|
217
|
+
method: 'tempo',
|
|
218
|
+
intent: 'session',
|
|
219
|
+
request: {
|
|
220
|
+
amount: '1000000',
|
|
221
|
+
recipient: payee,
|
|
222
|
+
currency: asset,
|
|
223
|
+
unitType: 'token',
|
|
224
|
+
methodDetails: {
|
|
225
|
+
chainId: chain.id,
|
|
226
|
+
escrowContract,
|
|
227
|
+
},
|
|
228
|
+
...overrides,
|
|
229
|
+
},
|
|
230
|
+
}) as any
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
beforeAll(async () => {
|
|
234
|
+
escrowContract = await deployEscrow()
|
|
235
|
+
await fundAccount({ address: payer.address, token: Addresses.pathUsd })
|
|
236
|
+
await fundAccount({ address: payer.address, token: asset })
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('auto deposit selection', () => {
|
|
240
|
+
test('context.depositRaw wins over everything', async () => {
|
|
241
|
+
const method = session({
|
|
242
|
+
getClient: () => client,
|
|
243
|
+
account: payer,
|
|
244
|
+
deposit: '99',
|
|
245
|
+
escrowContract,
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
const result = await method.createCredential({
|
|
249
|
+
challenge: makeLiveChallenge(),
|
|
250
|
+
context: { depositRaw: '5000000' },
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const cred = deserializePayload(result)
|
|
254
|
+
expect(cred.payload.action).toBe('open')
|
|
255
|
+
expect(cred.payload.channelId).toMatch(/^0x[0-9a-f]{64}$/)
|
|
256
|
+
if (cred.payload.action === 'open') {
|
|
257
|
+
expect(cred.payload.type).toBe('transaction')
|
|
258
|
+
expect(cred.payload.cumulativeAmount).toBe('1000000')
|
|
259
|
+
expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
260
|
+
}
|
|
261
|
+
expect(cred.source).toContain(`did:pkh:eip155:${chain.id}:`)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
test('suggestedDeposit capped by maxDeposit', async () => {
|
|
265
|
+
const method = session({
|
|
266
|
+
getClient: () => client,
|
|
267
|
+
account: payer,
|
|
268
|
+
maxDeposit: '5',
|
|
269
|
+
escrowContract,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const challenge = makeLiveChallenge({ suggestedDeposit: '10000000' })
|
|
273
|
+
|
|
274
|
+
const result = await method.createCredential({ challenge, context: {} })
|
|
275
|
+
|
|
276
|
+
const cred = deserializePayload(result)
|
|
277
|
+
expect(cred.payload.action).toBe('open')
|
|
278
|
+
if (cred.payload.action === 'open') {
|
|
279
|
+
expect(cred.payload.type).toBe('transaction')
|
|
280
|
+
expect(cred.payload.cumulativeAmount).toBe('1000000')
|
|
281
|
+
}
|
|
282
|
+
expect(cred.source).toContain(`did:pkh:eip155:${chain.id}:`)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('suggestedDeposit alone', async () => {
|
|
286
|
+
const method = session({
|
|
287
|
+
getClient: () => client,
|
|
288
|
+
account: payer,
|
|
289
|
+
deposit: '99',
|
|
290
|
+
escrowContract,
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const challenge = makeLiveChallenge({ suggestedDeposit: '7000000' })
|
|
294
|
+
|
|
295
|
+
const result = await method.createCredential({ challenge, context: {} })
|
|
296
|
+
|
|
297
|
+
const cred = deserializePayload(result)
|
|
298
|
+
expect(cred.payload.action).toBe('open')
|
|
299
|
+
if (cred.payload.action === 'open') {
|
|
300
|
+
expect(cred.payload.cumulativeAmount).toBe('1000000')
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('maxDeposit alone', async () => {
|
|
305
|
+
const method = session({
|
|
306
|
+
getClient: () => client,
|
|
307
|
+
account: payer,
|
|
308
|
+
maxDeposit: '10',
|
|
309
|
+
escrowContract,
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const result = await method.createCredential({
|
|
313
|
+
challenge: makeLiveChallenge(),
|
|
314
|
+
context: {},
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const cred = deserializePayload(result)
|
|
318
|
+
expect(cred.payload.action).toBe('open')
|
|
319
|
+
if (cred.payload.action === 'open') {
|
|
320
|
+
expect(cred.payload.cumulativeAmount).toBe('1000000')
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('parameters.deposit as last resort', async () => {
|
|
325
|
+
const method = session({
|
|
326
|
+
getClient: () => client,
|
|
327
|
+
account: payer,
|
|
328
|
+
deposit: '10',
|
|
329
|
+
escrowContract,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
const result = await method.createCredential({
|
|
333
|
+
challenge: makeLiveChallenge(),
|
|
334
|
+
context: {},
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const cred = deserializePayload(result)
|
|
338
|
+
expect(cred.payload.action).toBe('open')
|
|
339
|
+
if (cred.payload.action === 'open') {
|
|
340
|
+
expect(cred.payload.cumulativeAmount).toBe('1000000')
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('throws when no deposit source available', async () => {
|
|
345
|
+
const method = session({
|
|
346
|
+
getClient: () => client,
|
|
347
|
+
account: payer,
|
|
348
|
+
escrowContract,
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
await expect(
|
|
352
|
+
method.createCredential({ challenge: makeLiveChallenge(), context: {} }),
|
|
353
|
+
).rejects.toThrow()
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe('channel recovery', () => {
|
|
358
|
+
test('recovers channel via suggestedChannelId', async () => {
|
|
359
|
+
const salt = nextSalt()
|
|
360
|
+
const { channelId } = await openChannel({
|
|
361
|
+
escrow: escrowContract,
|
|
362
|
+
payer,
|
|
363
|
+
payee,
|
|
364
|
+
token: asset,
|
|
365
|
+
deposit: 10_000_000n,
|
|
366
|
+
salt,
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
const method = session({
|
|
370
|
+
getClient: () => client,
|
|
371
|
+
account: payer,
|
|
372
|
+
deposit: '10',
|
|
373
|
+
escrowContract,
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
const challenge = makeLiveChallenge({
|
|
377
|
+
methodDetails: {
|
|
378
|
+
chainId: chain.id,
|
|
379
|
+
escrowContract,
|
|
380
|
+
channelId,
|
|
381
|
+
},
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const result = await method.createCredential({ challenge, context: {} })
|
|
385
|
+
|
|
386
|
+
const cred = deserializePayload(result)
|
|
387
|
+
expect(cred.payload.action).toBe('voucher')
|
|
388
|
+
if (cred.payload.action === 'voucher') {
|
|
389
|
+
expect(cred.payload.channelId).toBe(channelId)
|
|
390
|
+
expect(cred.payload.cumulativeAmount).toBe('1000000')
|
|
391
|
+
expect(cred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
392
|
+
}
|
|
393
|
+
expect(cred.source).toContain(`did:pkh:eip155:${chain.id}:`)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
test('throws when explicit channelId cannot be recovered', async () => {
|
|
397
|
+
const method = session({
|
|
398
|
+
getClient: () => client,
|
|
399
|
+
account: payer,
|
|
400
|
+
deposit: '10',
|
|
401
|
+
escrowContract,
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
await expect(
|
|
405
|
+
method.createCredential({
|
|
406
|
+
challenge: makeLiveChallenge(),
|
|
407
|
+
context: {
|
|
408
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000bad',
|
|
409
|
+
},
|
|
410
|
+
}),
|
|
411
|
+
).rejects.toThrow('cannot be reused')
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
describe('cumulative tracking in auto mode', () => {
|
|
416
|
+
test('first call opens channel, second issues voucher with increased cumulative', async () => {
|
|
417
|
+
const updates: { cumulativeAmount: bigint }[] = []
|
|
418
|
+
const method = session({
|
|
419
|
+
getClient: () => client,
|
|
420
|
+
account: payer,
|
|
421
|
+
deposit: '10',
|
|
422
|
+
escrowContract,
|
|
423
|
+
onChannelUpdate: (entry) => updates.push({ cumulativeAmount: entry.cumulativeAmount }),
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const challenge = makeLiveChallenge()
|
|
427
|
+
|
|
428
|
+
const first = await method.createCredential({ challenge, context: {} })
|
|
429
|
+
const firstCred = deserializePayload(first)
|
|
430
|
+
expect(firstCred.payload.action).toBe('open')
|
|
431
|
+
if (firstCred.payload.action === 'open') {
|
|
432
|
+
expect(firstCred.payload.type).toBe('transaction')
|
|
433
|
+
expect(firstCred.payload.cumulativeAmount).toBe('1000000')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const second = await method.createCredential({ challenge, context: {} })
|
|
437
|
+
const secondCred = deserializePayload(second)
|
|
438
|
+
expect(secondCred.payload.action).toBe('voucher')
|
|
439
|
+
if (secondCred.payload.action === 'voucher') {
|
|
440
|
+
expect(secondCred.payload.channelId).toBe(firstCred.payload.channelId)
|
|
441
|
+
expect(secondCred.payload.cumulativeAmount).toBe('2000000')
|
|
442
|
+
expect(secondCred.payload.signature).toMatch(/^0x[0-9a-f]+$/)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
expect(updates.length).toBe(2)
|
|
446
|
+
expect(updates[0]!.cumulativeAmount).toBe(1_000_000n)
|
|
447
|
+
expect(updates[1]!.cumulativeAmount).toBe(2_000_000n)
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
describe('onChannelUpdate callback', () => {
|
|
452
|
+
test('fires on auto-manage open', async () => {
|
|
453
|
+
const updates: unknown[] = []
|
|
454
|
+
const method = session({
|
|
455
|
+
getClient: () => client,
|
|
456
|
+
account: payer,
|
|
457
|
+
deposit: '10',
|
|
458
|
+
escrowContract,
|
|
459
|
+
onChannelUpdate: (entry) => updates.push(entry),
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
await method.createCredential({ challenge: makeLiveChallenge(), context: {} })
|
|
463
|
+
|
|
464
|
+
expect(updates.length).toBeGreaterThan(0)
|
|
465
|
+
})
|
|
466
|
+
})
|
|
467
|
+
})
|
|
@@ -8,8 +8,8 @@ import * as Client from '../../viem/Client.js'
|
|
|
8
8
|
import * as z from '../../zod.js'
|
|
9
9
|
import * as defaults from '../internal/defaults.js'
|
|
10
10
|
import * as Methods from '../Methods.js'
|
|
11
|
-
import type {
|
|
12
|
-
import { signVoucher } from '../
|
|
11
|
+
import type { SessionCredentialPayload } from '../session/Types.js'
|
|
12
|
+
import { signVoucher } from '../session/Voucher.js'
|
|
13
13
|
import {
|
|
14
14
|
type ChannelEntry,
|
|
15
15
|
createOpenPayload,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
tryRecoverChannel,
|
|
20
20
|
} from './ChannelOps.js'
|
|
21
21
|
|
|
22
|
-
export const
|
|
22
|
+
export const sessionContextSchema = z.object({
|
|
23
23
|
account: z.optional(z.custom<Account.getResolver.Parameters['account']>()),
|
|
24
24
|
action: z.optional(z.enum(['open', 'topUp', 'voucher', 'close'])),
|
|
25
25
|
channelId: z.optional(z.string()),
|
|
@@ -32,7 +32,7 @@ export const streamContextSchema = z.object({
|
|
|
32
32
|
depositRaw: z.optional(z.string()),
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
export type
|
|
35
|
+
export type SessionContext = z.infer<typeof sessionContextSchema>
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Creates a session payment method for use with `Mppx.create()`.
|
|
@@ -112,7 +112,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
112
112
|
async function autoManageCredential(
|
|
113
113
|
challenge: Challenge.Challenge,
|
|
114
114
|
account: viem_Account,
|
|
115
|
-
context?:
|
|
115
|
+
context?: SessionContext,
|
|
116
116
|
): Promise<string> {
|
|
117
117
|
const md = challenge.request.methodDetails as
|
|
118
118
|
| { chainId?: number; escrowContract?: string; channelId?: string; feePayer?: boolean }
|
|
@@ -174,7 +174,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
let payload:
|
|
177
|
+
let payload: SessionCredentialPayload
|
|
178
178
|
|
|
179
179
|
if (entry?.opened) {
|
|
180
180
|
entry.cumulativeAmount += amount
|
|
@@ -212,7 +212,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
212
212
|
async function manualCredential(
|
|
213
213
|
challenge: Challenge.Challenge,
|
|
214
214
|
account: viem_Account,
|
|
215
|
-
context:
|
|
215
|
+
context: SessionContext,
|
|
216
216
|
): Promise<string> {
|
|
217
217
|
const md = challenge.request.methodDetails as
|
|
218
218
|
| { chainId?: number; escrowContract?: string; channelId?: string }
|
|
@@ -242,7 +242,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
242
242
|
const escrowContract = resolveEscrowCached(challenge, chainId, channelId)
|
|
243
243
|
escrowContractMap.set(channelId, escrowContract)
|
|
244
244
|
|
|
245
|
-
let payload:
|
|
245
|
+
let payload: SessionCredentialPayload
|
|
246
246
|
|
|
247
247
|
switch (action) {
|
|
248
248
|
case 'open': {
|
|
@@ -341,7 +341,7 @@ export function session(parameters: session.Parameters = {}) {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
return Method.toClient(Methods.session, {
|
|
344
|
-
context:
|
|
344
|
+
context: sessionContextSchema,
|
|
345
345
|
|
|
346
346
|
async createCredential({ challenge, context }) {
|
|
347
347
|
const chainId = challenge.request.methodDetails?.chainId ?? 0
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Hex } from 'viem'
|
|
2
2
|
import { describe, expect, test, vi } from 'vitest'
|
|
3
3
|
import * as Challenge from '../../Challenge.js'
|
|
4
|
-
import { formatNeedVoucherEvent, parseEvent } from '../
|
|
5
|
-
import type { NeedVoucherEvent,
|
|
4
|
+
import { formatNeedVoucherEvent, parseEvent } from '../session/Sse.js'
|
|
5
|
+
import type { NeedVoucherEvent, SessionReceipt } from '../session/Types.js'
|
|
6
6
|
import { sessionManager } from './SessionManager.js'
|
|
7
7
|
|
|
8
8
|
const channelId = '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex
|
|
@@ -139,7 +139,7 @@ describe('Session', () => {
|
|
|
139
139
|
acceptedCumulative: '2000000',
|
|
140
140
|
spent: '2000000',
|
|
141
141
|
units: 2,
|
|
142
|
-
} satisfies
|
|
142
|
+
} satisfies SessionReceipt)}\n\n`,
|
|
143
143
|
]
|
|
144
144
|
|
|
145
145
|
let callCount = 0
|
|
@@ -4,9 +4,9 @@ import type * as Challenge from '../../Challenge.js'
|
|
|
4
4
|
import * as Fetch from '../../client/internal/Fetch.js'
|
|
5
5
|
import type * as Account from '../../viem/Account.js'
|
|
6
6
|
import type * as Client from '../../viem/Client.js'
|
|
7
|
-
import {
|
|
8
|
-
import { parseEvent } from '../
|
|
9
|
-
import type {
|
|
7
|
+
import { deserializeSessionReceipt } from '../session/Receipt.js'
|
|
8
|
+
import { parseEvent } from '../session/Sse.js'
|
|
9
|
+
import type { SessionReceipt } from '../session/Types.js'
|
|
10
10
|
import type { ChannelEntry } from './ChannelOps.js'
|
|
11
11
|
import { session as sessionPlugin } from './Session.js'
|
|
12
12
|
|
|
@@ -20,15 +20,15 @@ export type SessionManager = {
|
|
|
20
20
|
sse(
|
|
21
21
|
input: RequestInfo | URL,
|
|
22
22
|
init?: RequestInit & {
|
|
23
|
-
onReceipt?: ((receipt:
|
|
23
|
+
onReceipt?: ((receipt: SessionReceipt) => void) | undefined
|
|
24
24
|
signal?: AbortSignal | undefined
|
|
25
25
|
},
|
|
26
26
|
): Promise<AsyncIterable<string>>
|
|
27
|
-
close(): Promise<
|
|
27
|
+
close(): Promise<SessionReceipt | undefined>
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export type PaymentResponse = Response & {
|
|
31
|
-
receipt:
|
|
31
|
+
receipt: SessionReceipt | null
|
|
32
32
|
challenge: Challenge.Challenge | null
|
|
33
33
|
channelId: Hex.Hex | null
|
|
34
34
|
cumulative: bigint
|
|
@@ -83,7 +83,7 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
83
83
|
|
|
84
84
|
function toPaymentResponse(response: Response): PaymentResponse {
|
|
85
85
|
const receiptHeader = response.headers.get('Payment-Receipt')
|
|
86
|
-
const receipt = receiptHeader ?
|
|
86
|
+
const receipt = receiptHeader ? deserializeSessionReceipt(receiptHeader) : null
|
|
87
87
|
return Object.assign(response, {
|
|
88
88
|
receipt,
|
|
89
89
|
challenge: lastChallenge,
|
|
@@ -241,14 +241,14 @@ export function sessionManager(parameters: sessionManager.Parameters): SessionMa
|
|
|
241
241
|
},
|
|
242
242
|
})
|
|
243
243
|
|
|
244
|
-
let receipt:
|
|
244
|
+
let receipt: SessionReceipt | undefined
|
|
245
245
|
if (lastUrl) {
|
|
246
246
|
const response = await fetchFn(lastUrl, {
|
|
247
247
|
method: 'POST',
|
|
248
248
|
headers: { Authorization: credential },
|
|
249
249
|
})
|
|
250
250
|
const receiptHeader = response.headers.get('Payment-Receipt')
|
|
251
|
-
if (receiptHeader) receipt =
|
|
251
|
+
if (receiptHeader) receipt = deserializeSessionReceipt(receiptHeader)
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
return receipt
|
package/src/tempo/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * as Methods from './Methods.js'
|
|
2
|
-
export * as
|
|
2
|
+
export * as Session from './session/index.js'
|
|
@@ -67,7 +67,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
67
67
|
recipient,
|
|
68
68
|
} as unknown as Defaults,
|
|
69
69
|
|
|
70
|
-
// TODO: dedupe `{charge,
|
|
70
|
+
// TODO: dedupe `{charge,session}.request`
|
|
71
71
|
async request({ credential, request }) {
|
|
72
72
|
const chainId = await (async () => {
|
|
73
73
|
if (request.chainId) return request.chainId
|