mppx 0.5.7 → 0.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/Challenge.d.ts +3 -2
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +27 -9
- package/dist/Challenge.js.map +1 -1
- package/dist/Method.d.ts +32 -14
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js.map +1 -1
- package/dist/Store.d.ts +68 -2
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +41 -4
- package/dist/Store.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js +7 -0
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +133 -70
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +8 -2
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +26 -1
- package/dist/server/Transport.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +13 -2
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +429 -4
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +28 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +89 -0
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +4 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +90 -66
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +3 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +3 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +8 -2
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/index.d.ts +1 -0
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js +1 -0
- package/dist/tempo/server/index.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/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +16 -6
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +12 -1
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +55 -14
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts +11 -2
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js +66 -25
- package/dist/tempo/session/Sse.js.map +1 -1
- package/dist/tempo/session/Ws.d.ts +87 -0
- package/dist/tempo/session/Ws.d.ts.map +1 -0
- package/dist/tempo/session/Ws.js +428 -0
- package/dist/tempo/session/Ws.js.map +1 -0
- package/dist/tempo/session/index.d.ts +1 -0
- package/dist/tempo/session/index.d.ts.map +1 -1
- package/dist/tempo/session/index.js +1 -0
- package/dist/tempo/session/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Challenge.test.ts +1 -1
- package/src/Challenge.ts +28 -9
- package/src/Method.ts +61 -20
- package/src/Store.test-d.ts +80 -2
- package/src/Store.test.ts +150 -13
- package/src/Store.ts +140 -3
- package/src/mcp-sdk/server/Transport.test.ts +12 -0
- package/src/mcp-sdk/server/Transport.ts +8 -0
- package/src/server/Mppx.test.ts +105 -0
- package/src/server/Mppx.ts +178 -88
- package/src/server/Transport.test.ts +31 -0
- package/src/server/Transport.ts +31 -2
- package/src/tempo/client/SessionManager.ts +510 -7
- package/src/tempo/internal/fee-payer.test.ts +115 -1
- package/src/tempo/internal/fee-payer.ts +138 -1
- package/src/tempo/server/AtomicStore.test-d.ts +34 -0
- package/src/tempo/server/Charge.test.ts +128 -0
- package/src/tempo/server/Charge.ts +118 -93
- package/src/tempo/server/Methods.ts +3 -0
- package/src/tempo/server/Session.test.ts +1044 -47
- package/src/tempo/server/Session.ts +8 -2
- package/src/tempo/server/Sse.test.ts +29 -0
- package/src/tempo/server/index.ts +1 -0
- package/src/tempo/server/internal/html/main.ts +9 -10
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/transport.ts +19 -6
- package/src/tempo/session/ChannelStore.test.ts +20 -1
- package/src/tempo/session/ChannelStore.ts +77 -14
- package/src/tempo/session/Sse.ts +77 -24
- package/src/tempo/session/Ws.test.ts +410 -0
- package/src/tempo/session/Ws.ts +563 -0
- package/src/tempo/session/index.ts +1 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as node_http from 'node:http'
|
|
2
|
+
|
|
1
3
|
import type { z } from 'mppx'
|
|
2
4
|
import { Challenge, Credential } from 'mppx'
|
|
3
5
|
import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
|
|
@@ -13,7 +15,9 @@ import {
|
|
|
13
15
|
import { waitForTransactionReceipt } from 'viem/actions'
|
|
14
16
|
import { Addresses } from 'viem/tempo'
|
|
15
17
|
import { beforeAll, beforeEach, describe, expect, expectTypeOf, test } from 'vp/test'
|
|
18
|
+
import { WebSocketServer } from 'ws'
|
|
16
19
|
import { nodeEnv } from '~test/config.js'
|
|
20
|
+
import * as Http from '~test/Http.js'
|
|
17
21
|
|
|
18
22
|
const isLocalnet = nodeEnv === 'localnet'
|
|
19
23
|
import {
|
|
@@ -32,6 +36,7 @@ import {
|
|
|
32
36
|
InsufficientBalanceError,
|
|
33
37
|
InvalidSignatureError,
|
|
34
38
|
} from '../../Errors.js'
|
|
39
|
+
import * as NodeRequest from '../../server/Request.js'
|
|
35
40
|
import * as Store from '../../Store.js'
|
|
36
41
|
import { sessionManager } from '../client/SessionManager.js'
|
|
37
42
|
import {
|
|
@@ -42,6 +47,7 @@ import type * as Methods from '../Methods.js'
|
|
|
42
47
|
import * as ChannelStore from '../session/ChannelStore.js'
|
|
43
48
|
import type { SessionReceipt } from '../session/Types.js'
|
|
44
49
|
import { signVoucher } from '../session/Voucher.js'
|
|
50
|
+
import * as TempoWs from '../session/Ws.js'
|
|
45
51
|
import { charge, session, settle } from './Session.js'
|
|
46
52
|
|
|
47
53
|
const payer = accounts[2]
|
|
@@ -61,7 +67,7 @@ beforeAll(async () => {
|
|
|
61
67
|
})
|
|
62
68
|
|
|
63
69
|
describe.runIf(isLocalnet)('session', () => {
|
|
64
|
-
let rawStore: Store.
|
|
70
|
+
let rawStore: Store.AtomicStore
|
|
65
71
|
let store: ChannelStore.ChannelStore
|
|
66
72
|
|
|
67
73
|
beforeEach(() => {
|
|
@@ -82,7 +88,7 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
function createServerWithStore(
|
|
85
|
-
customStore: Store.
|
|
91
|
+
customStore: Store.AtomicStore,
|
|
86
92
|
overrides: Partial<session.Parameters> = {},
|
|
87
93
|
) {
|
|
88
94
|
return session({
|
|
@@ -379,7 +385,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
379
385
|
})
|
|
380
386
|
|
|
381
387
|
// 2. Settle on-chain so settled becomes 5000000
|
|
382
|
-
const settleTxHash = await settle(store, client, channelId, {
|
|
388
|
+
const settleTxHash = await settle(store, client, channelId, {
|
|
389
|
+
escrowContract,
|
|
390
|
+
})
|
|
383
391
|
await waitForTransactionReceipt(client, { hash: settleTxHash })
|
|
384
392
|
expect((await store.getChannel(channelId))!.settledOnChain).toBe(5000000n)
|
|
385
393
|
|
|
@@ -436,7 +444,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
436
444
|
request: makeRequest(),
|
|
437
445
|
})
|
|
438
446
|
|
|
439
|
-
const settleTxHash = await settle(store, client, channelId, {
|
|
447
|
+
const settleTxHash = await settle(store, client, channelId, {
|
|
448
|
+
escrowContract,
|
|
449
|
+
})
|
|
440
450
|
await waitForTransactionReceipt(client, { hash: settleTxHash })
|
|
441
451
|
await store.updateChannel(channelId, () => null)
|
|
442
452
|
|
|
@@ -791,7 +801,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
791
801
|
const leakedSig = await signTestVoucher(victimChannelId, 5000000n)
|
|
792
802
|
await server.verify({
|
|
793
803
|
credential: {
|
|
794
|
-
challenge: makeChallenge({
|
|
804
|
+
challenge: makeChallenge({
|
|
805
|
+
id: 'c-victim',
|
|
806
|
+
channelId: victimChannelId,
|
|
807
|
+
}),
|
|
795
808
|
payload: {
|
|
796
809
|
action: 'voucher' as const,
|
|
797
810
|
channelId: victimChannelId,
|
|
@@ -823,7 +836,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
823
836
|
await expect(
|
|
824
837
|
server.verify({
|
|
825
838
|
credential: {
|
|
826
|
-
challenge: makeChallenge({
|
|
839
|
+
challenge: makeChallenge({
|
|
840
|
+
id: 'c-attack',
|
|
841
|
+
channelId: victimChannelId,
|
|
842
|
+
}),
|
|
827
843
|
payload: {
|
|
828
844
|
action: 'open' as const,
|
|
829
845
|
type: 'transaction' as const,
|
|
@@ -913,7 +929,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
913
929
|
// Now switch to a large TTL so subsequent vouchers use the cached path.
|
|
914
930
|
// The persisted closeRequestedAt should cause rejection without an
|
|
915
931
|
// on-chain re-query.
|
|
916
|
-
const server2 = createServer({
|
|
932
|
+
const server2 = createServer({
|
|
933
|
+
channelStateTtl: 60_000,
|
|
934
|
+
store: rawStore,
|
|
935
|
+
})
|
|
917
936
|
await expect(
|
|
918
937
|
server2.verify({
|
|
919
938
|
credential: {
|
|
@@ -945,7 +964,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
945
964
|
await expect(
|
|
946
965
|
server.verify({
|
|
947
966
|
credential: {
|
|
948
|
-
challenge: makeChallenge({
|
|
967
|
+
challenge: makeChallenge({
|
|
968
|
+
id: 'challenge-after-settle',
|
|
969
|
+
channelId,
|
|
970
|
+
}),
|
|
949
971
|
payload: {
|
|
950
972
|
action: 'voucher' as const,
|
|
951
973
|
channelId,
|
|
@@ -1223,7 +1245,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
1223
1245
|
await openServerChannel(server, channelId, serializedTransaction)
|
|
1224
1246
|
|
|
1225
1247
|
// Settle on-chain so settled becomes 1000000
|
|
1226
|
-
const settleTxHash = await settle(store, client, channelId, {
|
|
1248
|
+
const settleTxHash = await settle(store, client, channelId, {
|
|
1249
|
+
escrowContract,
|
|
1250
|
+
})
|
|
1227
1251
|
await waitForTransactionReceipt(client, { hash: settleTxHash })
|
|
1228
1252
|
|
|
1229
1253
|
// Try to close with voucher == on-chain settled — should be rejected
|
|
@@ -1694,7 +1718,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
1694
1718
|
request: makeRequest(),
|
|
1695
1719
|
})
|
|
1696
1720
|
|
|
1697
|
-
const settleTxHash = await settle(store, client, channelId, {
|
|
1721
|
+
const settleTxHash = await settle(store, client, channelId, {
|
|
1722
|
+
escrowContract,
|
|
1723
|
+
})
|
|
1698
1724
|
expect(settleTxHash).toMatch(/^0x/)
|
|
1699
1725
|
|
|
1700
1726
|
const ch = await store.getChannel(channelId)
|
|
@@ -1732,7 +1758,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
1732
1758
|
})
|
|
1733
1759
|
|
|
1734
1760
|
// Settle on-chain so onChain.settled = 5000000.
|
|
1735
|
-
const settleTxHash = await settle(store, client, channelId, {
|
|
1761
|
+
const settleTxHash = await settle(store, client, channelId, {
|
|
1762
|
+
escrowContract,
|
|
1763
|
+
})
|
|
1736
1764
|
await waitForTransactionReceipt(client, { hash: settleTxHash })
|
|
1737
1765
|
expect((await store.getChannel(channelId))!.settledOnChain).toBe(5000000n)
|
|
1738
1766
|
|
|
@@ -1786,7 +1814,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
1786
1814
|
request: makeRequest(),
|
|
1787
1815
|
})
|
|
1788
1816
|
|
|
1789
|
-
const settleTxHash2 = await settle(store, client, channelId, {
|
|
1817
|
+
const settleTxHash2 = await settle(store, client, channelId, {
|
|
1818
|
+
escrowContract,
|
|
1819
|
+
})
|
|
1790
1820
|
await waitForTransactionReceipt(client, { hash: settleTxHash2 })
|
|
1791
1821
|
|
|
1792
1822
|
// Wipe store.
|
|
@@ -1839,7 +1869,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
1839
1869
|
})
|
|
1840
1870
|
|
|
1841
1871
|
// Settle on-chain so onChain.settled = 3000000.
|
|
1842
|
-
const settleTxHash = await settle(store, client, channelId, {
|
|
1872
|
+
const settleTxHash = await settle(store, client, channelId, {
|
|
1873
|
+
escrowContract,
|
|
1874
|
+
})
|
|
1843
1875
|
await waitForTransactionReceipt(client, { hash: settleTxHash })
|
|
1844
1876
|
|
|
1845
1877
|
// Store still has the old record — settledOnChain is correct after settle.
|
|
@@ -1892,7 +1924,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
1892
1924
|
})
|
|
1893
1925
|
|
|
1894
1926
|
// Settle on-chain so onChain.settled = 5M.
|
|
1895
|
-
const settleTxHash = await settle(store, client, channelId, {
|
|
1927
|
+
const settleTxHash = await settle(store, client, channelId, {
|
|
1928
|
+
escrowContract,
|
|
1929
|
+
})
|
|
1896
1930
|
await waitForTransactionReceipt(client, { hash: settleTxHash })
|
|
1897
1931
|
|
|
1898
1932
|
// Store record still exists (no store loss), but spent is 0.
|
|
@@ -1948,7 +1982,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
1948
1982
|
request: makeRequest(),
|
|
1949
1983
|
})
|
|
1950
1984
|
|
|
1951
|
-
const settleTxHash3 = await settle(store, client, channelId, {
|
|
1985
|
+
const settleTxHash3 = await settle(store, client, channelId, {
|
|
1986
|
+
escrowContract,
|
|
1987
|
+
})
|
|
1952
1988
|
await waitForTransactionReceipt(client, { hash: settleTxHash3 })
|
|
1953
1989
|
await store.updateChannel(channelId, () => null)
|
|
1954
1990
|
|
|
@@ -1993,7 +2029,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
1993
2029
|
request: makeRequest(),
|
|
1994
2030
|
})
|
|
1995
2031
|
|
|
1996
|
-
const settleTxHash = await settle(store, client, channelId, {
|
|
2032
|
+
const settleTxHash = await settle(store, client, channelId, {
|
|
2033
|
+
escrowContract,
|
|
2034
|
+
})
|
|
1997
2035
|
await waitForTransactionReceipt(client, { hash: settleTxHash })
|
|
1998
2036
|
await store.updateChannel(channelId, () => null)
|
|
1999
2037
|
|
|
@@ -2057,7 +2095,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2057
2095
|
request: makeRequest(),
|
|
2058
2096
|
})
|
|
2059
2097
|
|
|
2060
|
-
const settleTxHash = await settle(store, client, channelId, {
|
|
2098
|
+
const settleTxHash = await settle(store, client, channelId, {
|
|
2099
|
+
escrowContract,
|
|
2100
|
+
})
|
|
2061
2101
|
await waitForTransactionReceipt(client, { hash: settleTxHash })
|
|
2062
2102
|
await store.updateChannel(channelId, () => null)
|
|
2063
2103
|
|
|
@@ -2120,7 +2160,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2120
2160
|
await expect(
|
|
2121
2161
|
server.verify({
|
|
2122
2162
|
credential: {
|
|
2123
|
-
challenge: makeChallenge({
|
|
2163
|
+
challenge: makeChallenge({
|
|
2164
|
+
id: 'voucher-invalid-compact',
|
|
2165
|
+
channelId,
|
|
2166
|
+
}),
|
|
2124
2167
|
payload: {
|
|
2125
2168
|
action: 'voucher' as const,
|
|
2126
2169
|
channelId,
|
|
@@ -2159,7 +2202,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2159
2202
|
amounts.map(async (amount, index) =>
|
|
2160
2203
|
server.verify({
|
|
2161
2204
|
credential: {
|
|
2162
|
-
challenge: makeChallenge({
|
|
2205
|
+
challenge: makeChallenge({
|
|
2206
|
+
id: `voucher-concurrency-${index}`,
|
|
2207
|
+
channelId,
|
|
2208
|
+
}),
|
|
2163
2209
|
payload: {
|
|
2164
2210
|
action: 'voucher' as const,
|
|
2165
2211
|
channelId,
|
|
@@ -2232,7 +2278,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2232
2278
|
|
|
2233
2279
|
await healthyServer.verify({
|
|
2234
2280
|
credential: {
|
|
2235
|
-
challenge: makeChallenge({
|
|
2281
|
+
challenge: makeChallenge({
|
|
2282
|
+
id: 'open-before-topup-crash',
|
|
2283
|
+
channelId,
|
|
2284
|
+
}),
|
|
2236
2285
|
payload: {
|
|
2237
2286
|
action: 'open' as const,
|
|
2238
2287
|
type: 'transaction' as const,
|
|
@@ -2278,7 +2327,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2278
2327
|
|
|
2279
2328
|
await healthyServer.verify({
|
|
2280
2329
|
credential: {
|
|
2281
|
-
challenge: makeChallenge({
|
|
2330
|
+
challenge: makeChallenge({
|
|
2331
|
+
id: 'reopen-after-topup-crash',
|
|
2332
|
+
channelId,
|
|
2333
|
+
}),
|
|
2282
2334
|
payload: {
|
|
2283
2335
|
action: 'open' as const,
|
|
2284
2336
|
type: 'transaction' as const,
|
|
@@ -2414,7 +2466,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2414
2466
|
|
|
2415
2467
|
await server.verify({
|
|
2416
2468
|
credential: {
|
|
2417
|
-
challenge: makeChallenge({
|
|
2469
|
+
challenge: makeChallenge({
|
|
2470
|
+
id: 'open-before-external-close',
|
|
2471
|
+
channelId,
|
|
2472
|
+
}),
|
|
2418
2473
|
payload: {
|
|
2419
2474
|
action: 'open' as const,
|
|
2420
2475
|
type: 'transaction' as const,
|
|
@@ -2439,7 +2494,10 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2439
2494
|
await expect(
|
|
2440
2495
|
server.verify({
|
|
2441
2496
|
credential: {
|
|
2442
|
-
challenge: makeChallenge({
|
|
2497
|
+
challenge: makeChallenge({
|
|
2498
|
+
id: 'close-after-external-finalize',
|
|
2499
|
+
channelId,
|
|
2500
|
+
}),
|
|
2443
2501
|
payload: {
|
|
2444
2502
|
action: 'close' as const,
|
|
2445
2503
|
channelId,
|
|
@@ -2463,7 +2521,9 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2463
2521
|
}),
|
|
2464
2522
|
payload: { action: 'voucher' },
|
|
2465
2523
|
},
|
|
2466
|
-
input: new Request('https://api.example.com/resource', {
|
|
2524
|
+
input: new Request('https://api.example.com/resource', {
|
|
2525
|
+
method: 'HEAD',
|
|
2526
|
+
}),
|
|
2467
2527
|
} as never)
|
|
2468
2528
|
|
|
2469
2529
|
expect(response).toBeUndefined()
|
|
@@ -2487,7 +2547,11 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2487
2547
|
|
|
2488
2548
|
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
|
|
2489
2549
|
const handler = createHandler()
|
|
2490
|
-
const route = handler.session({
|
|
2550
|
+
const route = handler.session({
|
|
2551
|
+
amount: '1',
|
|
2552
|
+
decimals: 6,
|
|
2553
|
+
unitType: 'token',
|
|
2554
|
+
})
|
|
2491
2555
|
|
|
2492
2556
|
const first = await route(new Request('https://api.example.com/resource'))
|
|
2493
2557
|
if (first.status !== 402) throw new Error('expected challenge')
|
|
@@ -2522,7 +2586,11 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
2522
2586
|
test('does not return Payment-Receipt on verification errors', async () => {
|
|
2523
2587
|
const { channelId, serializedTransaction } = await createSignedOpenTransaction(10000000n)
|
|
2524
2588
|
const handler = createHandler()
|
|
2525
|
-
const route = handler.session({
|
|
2589
|
+
const route = handler.session({
|
|
2590
|
+
amount: '1',
|
|
2591
|
+
decimals: 6,
|
|
2592
|
+
unitType: 'token',
|
|
2593
|
+
})
|
|
2526
2594
|
|
|
2527
2595
|
const first = await route(new Request('https://api.example.com/resource'))
|
|
2528
2596
|
if (first.status !== 402) throw new Error('expected challenge')
|
|
@@ -3059,6 +3127,58 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
3059
3127
|
expect(closeReceipt?.spent).toBe('4000000')
|
|
3060
3128
|
})
|
|
3061
3129
|
|
|
3130
|
+
test('refuses SSE voucher requests beyond local maxDeposit', async () => {
|
|
3131
|
+
const backingStore = Store.memory()
|
|
3132
|
+
const routeHandler = Mppx_server.create({
|
|
3133
|
+
methods: [
|
|
3134
|
+
tempo_server.session({
|
|
3135
|
+
store: backingStore,
|
|
3136
|
+
getClient: () => client,
|
|
3137
|
+
account: recipientAccount,
|
|
3138
|
+
currency,
|
|
3139
|
+
escrowContract,
|
|
3140
|
+
chainId: chain.id,
|
|
3141
|
+
sse: true,
|
|
3142
|
+
}),
|
|
3143
|
+
],
|
|
3144
|
+
realm: 'api.example.com',
|
|
3145
|
+
secretKey: 'secret',
|
|
3146
|
+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
|
|
3147
|
+
|
|
3148
|
+
const fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
3149
|
+
const request = new Request(input, init)
|
|
3150
|
+
const result = await routeHandler(request)
|
|
3151
|
+
if (result.status === 402) return result.challenge
|
|
3152
|
+
|
|
3153
|
+
if (request.headers.get('Accept')?.includes('text/event-stream')) {
|
|
3154
|
+
return result.withReceipt(async function* (stream) {
|
|
3155
|
+
await stream.charge()
|
|
3156
|
+
yield 'chunk-1'
|
|
3157
|
+
await stream.charge()
|
|
3158
|
+
yield 'chunk-2'
|
|
3159
|
+
})
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
return result.withReceipt(new Response('ok'))
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
const manager = sessionManager({
|
|
3166
|
+
account: payer,
|
|
3167
|
+
client,
|
|
3168
|
+
escrowContract,
|
|
3169
|
+
fetch,
|
|
3170
|
+
maxDeposit: '1',
|
|
3171
|
+
})
|
|
3172
|
+
|
|
3173
|
+
const stream = await manager.sse('https://api.example.com/stream')
|
|
3174
|
+
await expect(
|
|
3175
|
+
(async () => {
|
|
3176
|
+
for await (const _chunk of stream) {
|
|
3177
|
+
}
|
|
3178
|
+
})(),
|
|
3179
|
+
).rejects.toThrow('requested voucher amount 2000000 exceeds local maxDeposit 1000000')
|
|
3180
|
+
})
|
|
3181
|
+
|
|
3062
3182
|
test('behavior: charge withReceipt returns Response', async () => {
|
|
3063
3183
|
const handler = Mppx_server.create({
|
|
3064
3184
|
methods: [tempo_server.charge({ account: accounts[0], currency: asset })],
|
|
@@ -3077,6 +3197,851 @@ describe.runIf(isLocalnet)('session', () => {
|
|
|
3077
3197
|
}
|
|
3078
3198
|
})
|
|
3079
3199
|
})
|
|
3200
|
+
|
|
3201
|
+
describe('WebSocket', () => {
|
|
3202
|
+
test('open -> stream -> need-voucher -> resume -> close', async () => {
|
|
3203
|
+
const backingStore = Store.memory()
|
|
3204
|
+
const routeHandler = Mppx_server.create({
|
|
3205
|
+
methods: [
|
|
3206
|
+
tempo_server.session({
|
|
3207
|
+
store: backingStore,
|
|
3208
|
+
getClient: () => client,
|
|
3209
|
+
account: recipientAccount,
|
|
3210
|
+
currency,
|
|
3211
|
+
escrowContract,
|
|
3212
|
+
chainId: chain.id,
|
|
3213
|
+
}),
|
|
3214
|
+
],
|
|
3215
|
+
realm: 'api.example.com',
|
|
3216
|
+
secretKey: 'secret',
|
|
3217
|
+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
|
|
3218
|
+
|
|
3219
|
+
let voucherUpdates = 0
|
|
3220
|
+
const route = async (request: Request) => {
|
|
3221
|
+
if (request.method === 'POST' && request.headers.has('Authorization')) {
|
|
3222
|
+
try {
|
|
3223
|
+
const credential = Credential.fromRequest<any>(request)
|
|
3224
|
+
if (credential.payload?.action === 'voucher') voucherUpdates++
|
|
3225
|
+
} catch {}
|
|
3226
|
+
}
|
|
3227
|
+
return routeHandler(request)
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
const httpHandler = NodeRequest.toNodeListener(async (request) => {
|
|
3231
|
+
const result = await route(request)
|
|
3232
|
+
if (result.status === 402) return result.challenge
|
|
3233
|
+
return result.withReceipt(new Response('ok'))
|
|
3234
|
+
})
|
|
3235
|
+
|
|
3236
|
+
const nodeServer = node_http.createServer(httpHandler)
|
|
3237
|
+
const wsServer = new WebSocketServer({ noServer: true })
|
|
3238
|
+
|
|
3239
|
+
await new Promise<void>((resolve) => nodeServer.listen(0, resolve))
|
|
3240
|
+
const { port } = nodeServer.address() as { port: number }
|
|
3241
|
+
const server = Http.wrapServer(nodeServer, {
|
|
3242
|
+
port,
|
|
3243
|
+
url: `http://localhost:${port}`,
|
|
3244
|
+
})
|
|
3245
|
+
|
|
3246
|
+
wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
3247
|
+
void TempoWs.serve({
|
|
3248
|
+
socket,
|
|
3249
|
+
store: backingStore,
|
|
3250
|
+
url: `${server.url}/ws`,
|
|
3251
|
+
route,
|
|
3252
|
+
generate: async function* (stream: TempoWs.SessionController) {
|
|
3253
|
+
await stream.charge()
|
|
3254
|
+
yield 'chunk-1'
|
|
3255
|
+
await stream.charge()
|
|
3256
|
+
yield 'chunk-2'
|
|
3257
|
+
await stream.charge()
|
|
3258
|
+
yield 'chunk-3'
|
|
3259
|
+
},
|
|
3260
|
+
})
|
|
3261
|
+
})
|
|
3262
|
+
|
|
3263
|
+
nodeServer.on('upgrade', (req, socket, head) => {
|
|
3264
|
+
if (req.url !== '/ws') {
|
|
3265
|
+
socket.destroy()
|
|
3266
|
+
return
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3269
|
+
wsServer.handleUpgrade(req, socket, head, (websocket: import('ws').WebSocket) => {
|
|
3270
|
+
wsServer.emit('connection', websocket, req)
|
|
3271
|
+
})
|
|
3272
|
+
})
|
|
3273
|
+
|
|
3274
|
+
try {
|
|
3275
|
+
const manager = sessionManager({
|
|
3276
|
+
account: payer,
|
|
3277
|
+
client,
|
|
3278
|
+
escrowContract,
|
|
3279
|
+
fetch: globalThis.fetch,
|
|
3280
|
+
maxDeposit: '3',
|
|
3281
|
+
})
|
|
3282
|
+
|
|
3283
|
+
const ws = await manager.ws(`ws://localhost:${port}/ws`)
|
|
3284
|
+
const chunks: string[] = []
|
|
3285
|
+
|
|
3286
|
+
await new Promise<void>((resolve, reject) => {
|
|
3287
|
+
ws.addEventListener('message', (event) => {
|
|
3288
|
+
if (typeof event.data !== 'string') return
|
|
3289
|
+
chunks.push(event.data)
|
|
3290
|
+
})
|
|
3291
|
+
ws.addEventListener('close', () => resolve(), { once: true })
|
|
3292
|
+
ws.addEventListener('error', () => reject(new Error('websocket stream failed')), {
|
|
3293
|
+
once: true,
|
|
3294
|
+
})
|
|
3295
|
+
})
|
|
3296
|
+
|
|
3297
|
+
expect(chunks).toEqual(['chunk-1', 'chunk-2', 'chunk-3'])
|
|
3298
|
+
expect(voucherUpdates).toBeGreaterThan(0)
|
|
3299
|
+
|
|
3300
|
+
const closeReceipt = await manager.close()
|
|
3301
|
+
expect(closeReceipt?.status).toBe('success')
|
|
3302
|
+
expect(closeReceipt?.spent).toBe('3000000')
|
|
3303
|
+
|
|
3304
|
+
const channelId = manager.channelId
|
|
3305
|
+
expect(channelId).toBeTruthy()
|
|
3306
|
+
|
|
3307
|
+
const persisted = await ChannelStore.fromStore(backingStore).getChannel(channelId!)
|
|
3308
|
+
expect(persisted?.finalized).toBe(true)
|
|
3309
|
+
} finally {
|
|
3310
|
+
wsServer.close()
|
|
3311
|
+
server.close()
|
|
3312
|
+
}
|
|
3313
|
+
})
|
|
3314
|
+
|
|
3315
|
+
test('treats control-shaped application payloads as content', async () => {
|
|
3316
|
+
const backingStore = Store.memory()
|
|
3317
|
+
const routeHandler = Mppx_server.create({
|
|
3318
|
+
methods: [
|
|
3319
|
+
tempo_server.session({
|
|
3320
|
+
store: backingStore,
|
|
3321
|
+
getClient: () => client,
|
|
3322
|
+
account: recipientAccount,
|
|
3323
|
+
currency,
|
|
3324
|
+
escrowContract,
|
|
3325
|
+
chainId: chain.id,
|
|
3326
|
+
}),
|
|
3327
|
+
],
|
|
3328
|
+
realm: 'api.example.com',
|
|
3329
|
+
secretKey: 'secret',
|
|
3330
|
+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
|
|
3331
|
+
|
|
3332
|
+
let voucherUpdates = 0
|
|
3333
|
+
const route = async (request: Request) => {
|
|
3334
|
+
if (request.method === 'POST' && request.headers.has('Authorization')) {
|
|
3335
|
+
try {
|
|
3336
|
+
const credential = Credential.fromRequest<any>(request)
|
|
3337
|
+
if (credential.payload?.action === 'voucher') voucherUpdates++
|
|
3338
|
+
} catch {}
|
|
3339
|
+
}
|
|
3340
|
+
return routeHandler(request)
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
const httpHandler = NodeRequest.toNodeListener(async (request) => {
|
|
3344
|
+
const result = await route(request)
|
|
3345
|
+
if (result.status === 402) return result.challenge
|
|
3346
|
+
return result.withReceipt(new Response('ok'))
|
|
3347
|
+
})
|
|
3348
|
+
|
|
3349
|
+
const nodeServer = node_http.createServer(httpHandler)
|
|
3350
|
+
const wsServer = new WebSocketServer({ noServer: true })
|
|
3351
|
+
|
|
3352
|
+
await new Promise<void>((resolve) => nodeServer.listen(0, resolve))
|
|
3353
|
+
const { port } = nodeServer.address() as { port: number }
|
|
3354
|
+
const server = Http.wrapServer(nodeServer, {
|
|
3355
|
+
port,
|
|
3356
|
+
url: `http://localhost:${port}`,
|
|
3357
|
+
})
|
|
3358
|
+
|
|
3359
|
+
const controlLookingChunk = JSON.stringify({
|
|
3360
|
+
mpp: 'payment-need-voucher',
|
|
3361
|
+
data: {
|
|
3362
|
+
channelId: '0x' + 'aa'.repeat(32),
|
|
3363
|
+
requiredCumulative: '9000000',
|
|
3364
|
+
acceptedCumulative: '0',
|
|
3365
|
+
deposit: '9000000',
|
|
3366
|
+
},
|
|
3367
|
+
})
|
|
3368
|
+
|
|
3369
|
+
wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
3370
|
+
void TempoWs.serve({
|
|
3371
|
+
socket,
|
|
3372
|
+
store: backingStore,
|
|
3373
|
+
url: `${server.url}/ws`,
|
|
3374
|
+
route,
|
|
3375
|
+
generate: async function* (stream: TempoWs.SessionController) {
|
|
3376
|
+
await stream.charge()
|
|
3377
|
+
yield controlLookingChunk
|
|
3378
|
+
},
|
|
3379
|
+
})
|
|
3380
|
+
})
|
|
3381
|
+
|
|
3382
|
+
nodeServer.on('upgrade', (req, socket, head) => {
|
|
3383
|
+
if (req.url !== '/ws') {
|
|
3384
|
+
socket.destroy()
|
|
3385
|
+
return
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
wsServer.handleUpgrade(req, socket, head, (websocket: import('ws').WebSocket) => {
|
|
3389
|
+
wsServer.emit('connection', websocket, req)
|
|
3390
|
+
})
|
|
3391
|
+
})
|
|
3392
|
+
|
|
3393
|
+
try {
|
|
3394
|
+
const manager = sessionManager({
|
|
3395
|
+
account: payer,
|
|
3396
|
+
client,
|
|
3397
|
+
escrowContract,
|
|
3398
|
+
fetch: globalThis.fetch,
|
|
3399
|
+
maxDeposit: '1',
|
|
3400
|
+
})
|
|
3401
|
+
|
|
3402
|
+
const ws = await manager.ws(`ws://localhost:${port}/ws`)
|
|
3403
|
+
const chunks: string[] = []
|
|
3404
|
+
|
|
3405
|
+
await new Promise<void>((resolve, reject) => {
|
|
3406
|
+
ws.addEventListener('message', (event) => {
|
|
3407
|
+
if (typeof event.data !== 'string') return
|
|
3408
|
+
chunks.push(event.data)
|
|
3409
|
+
})
|
|
3410
|
+
ws.addEventListener('close', () => resolve(), { once: true })
|
|
3411
|
+
ws.addEventListener('error', () => reject(new Error('websocket stream failed')), {
|
|
3412
|
+
once: true,
|
|
3413
|
+
})
|
|
3414
|
+
})
|
|
3415
|
+
|
|
3416
|
+
expect(chunks).toEqual([controlLookingChunk])
|
|
3417
|
+
expect(voucherUpdates).toBe(0)
|
|
3418
|
+
|
|
3419
|
+
const closeReceipt = await manager.close()
|
|
3420
|
+
expect(closeReceipt?.status).toBe('success')
|
|
3421
|
+
expect(closeReceipt?.spent).toBe('1000000')
|
|
3422
|
+
} finally {
|
|
3423
|
+
wsServer.close()
|
|
3424
|
+
server.close()
|
|
3425
|
+
}
|
|
3426
|
+
})
|
|
3427
|
+
|
|
3428
|
+
test('close() stops the stream and application listeners never receive payment control frames', async () => {
|
|
3429
|
+
const backingStore = Store.memory()
|
|
3430
|
+
const routeHandler = Mppx_server.create({
|
|
3431
|
+
methods: [
|
|
3432
|
+
tempo_server.session({
|
|
3433
|
+
store: backingStore,
|
|
3434
|
+
getClient: () => client,
|
|
3435
|
+
account: recipientAccount,
|
|
3436
|
+
currency,
|
|
3437
|
+
escrowContract,
|
|
3438
|
+
chainId: chain.id,
|
|
3439
|
+
}),
|
|
3440
|
+
],
|
|
3441
|
+
realm: 'api.example.com',
|
|
3442
|
+
secretKey: 'secret',
|
|
3443
|
+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
|
|
3444
|
+
|
|
3445
|
+
const route = (request: Request) => routeHandler(request)
|
|
3446
|
+
|
|
3447
|
+
const httpHandler = NodeRequest.toNodeListener(async (request) => {
|
|
3448
|
+
const result = await route(request)
|
|
3449
|
+
if (result.status === 402) return result.challenge
|
|
3450
|
+
return result.withReceipt(new Response('ok'))
|
|
3451
|
+
})
|
|
3452
|
+
|
|
3453
|
+
const nodeServer = node_http.createServer(httpHandler)
|
|
3454
|
+
const wsServer = new WebSocketServer({ noServer: true })
|
|
3455
|
+
|
|
3456
|
+
await new Promise<void>((resolve) => nodeServer.listen(0, resolve))
|
|
3457
|
+
const { port } = nodeServer.address() as { port: number }
|
|
3458
|
+
const server = Http.wrapServer(nodeServer, {
|
|
3459
|
+
port,
|
|
3460
|
+
url: `http://localhost:${port}`,
|
|
3461
|
+
})
|
|
3462
|
+
|
|
3463
|
+
wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
3464
|
+
void TempoWs.serve({
|
|
3465
|
+
socket,
|
|
3466
|
+
store: backingStore,
|
|
3467
|
+
url: `${server.url}/ws`,
|
|
3468
|
+
route,
|
|
3469
|
+
generate: async function* (stream: TempoWs.SessionController) {
|
|
3470
|
+
await stream.charge()
|
|
3471
|
+
yield 'chunk-1'
|
|
3472
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
3473
|
+
await stream.charge()
|
|
3474
|
+
yield 'chunk-2'
|
|
3475
|
+
await stream.charge()
|
|
3476
|
+
yield 'chunk-3'
|
|
3477
|
+
},
|
|
3478
|
+
})
|
|
3479
|
+
})
|
|
3480
|
+
|
|
3481
|
+
nodeServer.on('upgrade', (req, socket, head) => {
|
|
3482
|
+
if (req.url !== '/ws') {
|
|
3483
|
+
socket.destroy()
|
|
3484
|
+
return
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
wsServer.handleUpgrade(req, socket, head, (websocket: import('ws').WebSocket) => {
|
|
3488
|
+
wsServer.emit('connection', websocket, req)
|
|
3489
|
+
})
|
|
3490
|
+
})
|
|
3491
|
+
|
|
3492
|
+
try {
|
|
3493
|
+
const manager = sessionManager({
|
|
3494
|
+
account: payer,
|
|
3495
|
+
client,
|
|
3496
|
+
escrowContract,
|
|
3497
|
+
fetch: globalThis.fetch,
|
|
3498
|
+
maxDeposit: '3',
|
|
3499
|
+
})
|
|
3500
|
+
|
|
3501
|
+
let receiptCount = 0
|
|
3502
|
+
let closePromise: Promise<SessionReceipt | undefined> | undefined
|
|
3503
|
+
const ws = await manager.ws(`ws://localhost:${port}/ws`, {
|
|
3504
|
+
onReceipt() {
|
|
3505
|
+
receiptCount++
|
|
3506
|
+
if (receiptCount === 2 && !closePromise) closePromise = manager.close()
|
|
3507
|
+
},
|
|
3508
|
+
})
|
|
3509
|
+
|
|
3510
|
+
const chunks: string[] = []
|
|
3511
|
+
await new Promise<void>((resolve, reject) => {
|
|
3512
|
+
ws.addEventListener('message', (event) => {
|
|
3513
|
+
if (typeof event.data !== 'string') return
|
|
3514
|
+
chunks.push(event.data)
|
|
3515
|
+
})
|
|
3516
|
+
ws.addEventListener('close', () => resolve(), { once: true })
|
|
3517
|
+
ws.addEventListener('error', () => reject(new Error('websocket stream failed')), {
|
|
3518
|
+
once: true,
|
|
3519
|
+
})
|
|
3520
|
+
})
|
|
3521
|
+
|
|
3522
|
+
const closeReceipt = await closePromise
|
|
3523
|
+
expect(closeReceipt?.status).toBe('success')
|
|
3524
|
+
expect(closeReceipt?.txHash).toBeTruthy()
|
|
3525
|
+
expect(closeReceipt?.spent).toBe('2000000')
|
|
3526
|
+
expect(chunks).toEqual(['chunk-1', 'chunk-2'])
|
|
3527
|
+
|
|
3528
|
+
const channelId = manager.channelId
|
|
3529
|
+
expect(channelId).toBeTruthy()
|
|
3530
|
+
|
|
3531
|
+
const persisted = await ChannelStore.fromStore(backingStore).getChannel(channelId!)
|
|
3532
|
+
expect(persisted?.finalized).toBe(true)
|
|
3533
|
+
} finally {
|
|
3534
|
+
wsServer.close()
|
|
3535
|
+
server.close()
|
|
3536
|
+
}
|
|
3537
|
+
})
|
|
3538
|
+
|
|
3539
|
+
test('rejects websocket receipts bound to a different channel', async () => {
|
|
3540
|
+
const routeHandler = Mppx_server.create({
|
|
3541
|
+
methods: [
|
|
3542
|
+
tempo_server.session({
|
|
3543
|
+
store: Store.memory(),
|
|
3544
|
+
getClient: () => client,
|
|
3545
|
+
account: recipientAccount,
|
|
3546
|
+
currency,
|
|
3547
|
+
escrowContract,
|
|
3548
|
+
chainId: chain.id,
|
|
3549
|
+
}),
|
|
3550
|
+
],
|
|
3551
|
+
realm: 'api.example.com',
|
|
3552
|
+
secretKey: 'secret',
|
|
3553
|
+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
|
|
3554
|
+
|
|
3555
|
+
const httpHandler = NodeRequest.toNodeListener(async (request) => {
|
|
3556
|
+
const result = await routeHandler(request)
|
|
3557
|
+
if (result.status === 402) return result.challenge
|
|
3558
|
+
return result.withReceipt(new Response('ok'))
|
|
3559
|
+
})
|
|
3560
|
+
|
|
3561
|
+
const nodeServer = node_http.createServer(httpHandler)
|
|
3562
|
+
const wsServer = new WebSocketServer({ noServer: true })
|
|
3563
|
+
const wrongChannelId = `0x${'11'.repeat(32)}` as Hex
|
|
3564
|
+
|
|
3565
|
+
await new Promise<void>((resolve) => nodeServer.listen(0, resolve))
|
|
3566
|
+
const { port } = nodeServer.address() as { port: number }
|
|
3567
|
+
const server = Http.wrapServer(nodeServer, {
|
|
3568
|
+
port,
|
|
3569
|
+
url: `http://localhost:${port}`,
|
|
3570
|
+
})
|
|
3571
|
+
|
|
3572
|
+
wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
3573
|
+
socket.once('message', (data) => {
|
|
3574
|
+
const raw = data.toString()
|
|
3575
|
+
const message = TempoWs.parseMessage(raw)
|
|
3576
|
+
if (!message || message.mpp !== 'authorization') return
|
|
3577
|
+
|
|
3578
|
+
const credential = Credential.deserialize<any>(message.authorization)
|
|
3579
|
+
socket.send(
|
|
3580
|
+
TempoWs.formatReceiptMessage({
|
|
3581
|
+
method: 'tempo',
|
|
3582
|
+
intent: 'session',
|
|
3583
|
+
status: 'success',
|
|
3584
|
+
timestamp: new Date().toISOString(),
|
|
3585
|
+
reference: wrongChannelId,
|
|
3586
|
+
challengeId: credential.challenge.id,
|
|
3587
|
+
channelId: wrongChannelId,
|
|
3588
|
+
acceptedCumulative: '1000000',
|
|
3589
|
+
spent: '0',
|
|
3590
|
+
units: 0,
|
|
3591
|
+
}),
|
|
3592
|
+
)
|
|
3593
|
+
})
|
|
3594
|
+
})
|
|
3595
|
+
|
|
3596
|
+
nodeServer.on('upgrade', (req, socket, head) => {
|
|
3597
|
+
if (req.url !== '/ws') {
|
|
3598
|
+
socket.destroy()
|
|
3599
|
+
return
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
wsServer.handleUpgrade(req, socket, head, (websocket: import('ws').WebSocket) => {
|
|
3603
|
+
wsServer.emit('connection', websocket, req)
|
|
3604
|
+
})
|
|
3605
|
+
})
|
|
3606
|
+
|
|
3607
|
+
try {
|
|
3608
|
+
const manager = sessionManager({
|
|
3609
|
+
account: payer,
|
|
3610
|
+
client,
|
|
3611
|
+
escrowContract,
|
|
3612
|
+
fetch: globalThis.fetch,
|
|
3613
|
+
maxDeposit: '1',
|
|
3614
|
+
})
|
|
3615
|
+
|
|
3616
|
+
await expect(manager.ws(`ws://localhost:${port}/ws`)).rejects.toThrow(
|
|
3617
|
+
'received mismatched payment-receipt frame',
|
|
3618
|
+
)
|
|
3619
|
+
} finally {
|
|
3620
|
+
wsServer.close()
|
|
3621
|
+
server.close()
|
|
3622
|
+
}
|
|
3623
|
+
})
|
|
3624
|
+
|
|
3625
|
+
test('refuses websocket voucher requests beyond local maxDeposit', async () => {
|
|
3626
|
+
const backingStore = Store.memory()
|
|
3627
|
+
const routeHandler = Mppx_server.create({
|
|
3628
|
+
methods: [
|
|
3629
|
+
tempo_server.session({
|
|
3630
|
+
store: backingStore,
|
|
3631
|
+
getClient: () => client,
|
|
3632
|
+
account: recipientAccount,
|
|
3633
|
+
currency,
|
|
3634
|
+
escrowContract,
|
|
3635
|
+
chainId: chain.id,
|
|
3636
|
+
}),
|
|
3637
|
+
],
|
|
3638
|
+
realm: 'api.example.com',
|
|
3639
|
+
secretKey: 'secret',
|
|
3640
|
+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
|
|
3641
|
+
|
|
3642
|
+
const route = (request: Request) => routeHandler(request)
|
|
3643
|
+
|
|
3644
|
+
const httpHandler = NodeRequest.toNodeListener(async (request) => {
|
|
3645
|
+
const result = await route(request)
|
|
3646
|
+
if (result.status === 402) return result.challenge
|
|
3647
|
+
return result.withReceipt(new Response('ok'))
|
|
3648
|
+
})
|
|
3649
|
+
|
|
3650
|
+
const nodeServer = node_http.createServer(httpHandler)
|
|
3651
|
+
const wsServer = new WebSocketServer({ noServer: true })
|
|
3652
|
+
|
|
3653
|
+
await new Promise<void>((resolve) => nodeServer.listen(0, resolve))
|
|
3654
|
+
const { port } = nodeServer.address() as { port: number }
|
|
3655
|
+
const server = Http.wrapServer(nodeServer, {
|
|
3656
|
+
port,
|
|
3657
|
+
url: `http://localhost:${port}`,
|
|
3658
|
+
})
|
|
3659
|
+
|
|
3660
|
+
wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
3661
|
+
void TempoWs.serve({
|
|
3662
|
+
socket,
|
|
3663
|
+
store: backingStore,
|
|
3664
|
+
url: `${server.url}/ws`,
|
|
3665
|
+
route,
|
|
3666
|
+
generate: async function* (stream: TempoWs.SessionController) {
|
|
3667
|
+
await stream.charge()
|
|
3668
|
+
yield 'chunk-1'
|
|
3669
|
+
await stream.charge()
|
|
3670
|
+
yield 'chunk-2'
|
|
3671
|
+
},
|
|
3672
|
+
})
|
|
3673
|
+
})
|
|
3674
|
+
|
|
3675
|
+
nodeServer.on('upgrade', (req, socket, head) => {
|
|
3676
|
+
if (req.url !== '/ws') {
|
|
3677
|
+
socket.destroy()
|
|
3678
|
+
return
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
wsServer.handleUpgrade(req, socket, head, (websocket: import('ws').WebSocket) => {
|
|
3682
|
+
wsServer.emit('connection', websocket, req)
|
|
3683
|
+
})
|
|
3684
|
+
})
|
|
3685
|
+
|
|
3686
|
+
try {
|
|
3687
|
+
const manager = sessionManager({
|
|
3688
|
+
account: payer,
|
|
3689
|
+
client,
|
|
3690
|
+
escrowContract,
|
|
3691
|
+
fetch: globalThis.fetch,
|
|
3692
|
+
maxDeposit: '1',
|
|
3693
|
+
})
|
|
3694
|
+
|
|
3695
|
+
const ws = await manager.ws(`ws://localhost:${port}/ws`)
|
|
3696
|
+
const closeEvent = await new Promise<{ code: number; reason: string }>(
|
|
3697
|
+
(resolve, reject) => {
|
|
3698
|
+
ws.addEventListener(
|
|
3699
|
+
'close',
|
|
3700
|
+
(event) => resolve({ code: event.code, reason: event.reason }),
|
|
3701
|
+
{ once: true },
|
|
3702
|
+
)
|
|
3703
|
+
ws.addEventListener('error', () => reject(new Error('websocket stream failed')), {
|
|
3704
|
+
once: true,
|
|
3705
|
+
})
|
|
3706
|
+
},
|
|
3707
|
+
)
|
|
3708
|
+
|
|
3709
|
+
expect(closeEvent.code).toBe(3008)
|
|
3710
|
+
expect(closeEvent.reason).toBe(
|
|
3711
|
+
'requested voucher amount 2000000 exceeds local maxDeposit 1000000',
|
|
3712
|
+
)
|
|
3713
|
+
} finally {
|
|
3714
|
+
wsServer.close()
|
|
3715
|
+
server.close()
|
|
3716
|
+
}
|
|
3717
|
+
})
|
|
3718
|
+
|
|
3719
|
+
test('rejects close-ready receipts beyond local voucher state', async () => {
|
|
3720
|
+
const routeHandler = Mppx_server.create({
|
|
3721
|
+
methods: [
|
|
3722
|
+
tempo_server.session({
|
|
3723
|
+
store: Store.memory(),
|
|
3724
|
+
getClient: () => client,
|
|
3725
|
+
account: recipientAccount,
|
|
3726
|
+
currency,
|
|
3727
|
+
escrowContract,
|
|
3728
|
+
chainId: chain.id,
|
|
3729
|
+
}),
|
|
3730
|
+
],
|
|
3731
|
+
realm: 'api.example.com',
|
|
3732
|
+
secretKey: 'secret',
|
|
3733
|
+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
|
|
3734
|
+
|
|
3735
|
+
const httpHandler = NodeRequest.toNodeListener(async (request) => {
|
|
3736
|
+
const result = await routeHandler(request)
|
|
3737
|
+
if (result.status === 402) return result.challenge
|
|
3738
|
+
return result.withReceipt(new Response('ok'))
|
|
3739
|
+
})
|
|
3740
|
+
|
|
3741
|
+
const nodeServer = node_http.createServer(httpHandler)
|
|
3742
|
+
const wsServer = new WebSocketServer({ noServer: true })
|
|
3743
|
+
|
|
3744
|
+
await new Promise<void>((resolve) => nodeServer.listen(0, resolve))
|
|
3745
|
+
const { port } = nodeServer.address() as { port: number }
|
|
3746
|
+
const server = Http.wrapServer(nodeServer, {
|
|
3747
|
+
port,
|
|
3748
|
+
url: `http://localhost:${port}`,
|
|
3749
|
+
})
|
|
3750
|
+
|
|
3751
|
+
wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
3752
|
+
let openCredential: any
|
|
3753
|
+
|
|
3754
|
+
socket.on('message', (data) => {
|
|
3755
|
+
const raw = data.toString()
|
|
3756
|
+
const message = TempoWs.parseMessage(raw)
|
|
3757
|
+
if (!message) return
|
|
3758
|
+
|
|
3759
|
+
if (message.mpp === 'authorization') {
|
|
3760
|
+
openCredential = Credential.deserialize<any>(message.authorization)
|
|
3761
|
+
socket.send(
|
|
3762
|
+
TempoWs.formatReceiptMessage({
|
|
3763
|
+
method: 'tempo',
|
|
3764
|
+
intent: 'session',
|
|
3765
|
+
status: 'success',
|
|
3766
|
+
timestamp: new Date().toISOString(),
|
|
3767
|
+
reference: openCredential.payload.channelId,
|
|
3768
|
+
challengeId: openCredential.challenge.id,
|
|
3769
|
+
channelId: openCredential.payload.channelId,
|
|
3770
|
+
acceptedCumulative: openCredential.payload.cumulativeAmount,
|
|
3771
|
+
spent: '0',
|
|
3772
|
+
units: 0,
|
|
3773
|
+
}),
|
|
3774
|
+
)
|
|
3775
|
+
return
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
if (message.mpp === 'payment-close-request' && openCredential) {
|
|
3779
|
+
socket.send(
|
|
3780
|
+
TempoWs.formatCloseReadyMessage({
|
|
3781
|
+
method: 'tempo',
|
|
3782
|
+
intent: 'session',
|
|
3783
|
+
status: 'success',
|
|
3784
|
+
timestamp: new Date().toISOString(),
|
|
3785
|
+
reference: openCredential.payload.channelId,
|
|
3786
|
+
challengeId: openCredential.challenge.id,
|
|
3787
|
+
channelId: openCredential.payload.channelId,
|
|
3788
|
+
acceptedCumulative: openCredential.payload.cumulativeAmount,
|
|
3789
|
+
spent: '9000000',
|
|
3790
|
+
units: 1,
|
|
3791
|
+
}),
|
|
3792
|
+
)
|
|
3793
|
+
}
|
|
3794
|
+
})
|
|
3795
|
+
})
|
|
3796
|
+
|
|
3797
|
+
nodeServer.on('upgrade', (req, socket, head) => {
|
|
3798
|
+
if (req.url !== '/ws') {
|
|
3799
|
+
socket.destroy()
|
|
3800
|
+
return
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
wsServer.handleUpgrade(req, socket, head, (websocket: import('ws').WebSocket) => {
|
|
3804
|
+
wsServer.emit('connection', websocket, req)
|
|
3805
|
+
})
|
|
3806
|
+
})
|
|
3807
|
+
|
|
3808
|
+
try {
|
|
3809
|
+
const manager = sessionManager({
|
|
3810
|
+
account: payer,
|
|
3811
|
+
client,
|
|
3812
|
+
escrowContract,
|
|
3813
|
+
fetch: globalThis.fetch,
|
|
3814
|
+
maxDeposit: '1',
|
|
3815
|
+
})
|
|
3816
|
+
|
|
3817
|
+
await manager.ws(`ws://localhost:${port}/ws`)
|
|
3818
|
+
await expect(manager.close()).rejects.toThrow(
|
|
3819
|
+
'received payment-close-ready beyond local voucher state',
|
|
3820
|
+
)
|
|
3821
|
+
} finally {
|
|
3822
|
+
wsServer.close()
|
|
3823
|
+
server.close()
|
|
3824
|
+
}
|
|
3825
|
+
})
|
|
3826
|
+
|
|
3827
|
+
test('fallback close after socket death signs for delivered amount, not full voucher', async () => {
|
|
3828
|
+
const backingStore = Store.memory()
|
|
3829
|
+
const routeHandler = Mppx_server.create({
|
|
3830
|
+
methods: [
|
|
3831
|
+
tempo_server.session({
|
|
3832
|
+
store: backingStore,
|
|
3833
|
+
getClient: () => client,
|
|
3834
|
+
account: recipientAccount,
|
|
3835
|
+
currency,
|
|
3836
|
+
escrowContract,
|
|
3837
|
+
chainId: chain.id,
|
|
3838
|
+
}),
|
|
3839
|
+
],
|
|
3840
|
+
realm: 'api.example.com',
|
|
3841
|
+
secretKey: 'secret',
|
|
3842
|
+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
|
|
3843
|
+
|
|
3844
|
+
const route = (request: Request) => routeHandler(request)
|
|
3845
|
+
|
|
3846
|
+
const httpHandler = NodeRequest.toNodeListener(async (request) => {
|
|
3847
|
+
const result = await route(request)
|
|
3848
|
+
if (result.status === 402) return result.challenge
|
|
3849
|
+
return result.withReceipt(new Response('ok'))
|
|
3850
|
+
})
|
|
3851
|
+
|
|
3852
|
+
const nodeServer = node_http.createServer(httpHandler)
|
|
3853
|
+
const wsServer = new WebSocketServer({ noServer: true })
|
|
3854
|
+
let serverSocket: import('ws').WebSocket | null = null
|
|
3855
|
+
|
|
3856
|
+
await new Promise<void>((resolve) => nodeServer.listen(0, resolve))
|
|
3857
|
+
const { port } = nodeServer.address() as { port: number }
|
|
3858
|
+
const server = Http.wrapServer(nodeServer, {
|
|
3859
|
+
port,
|
|
3860
|
+
url: `http://localhost:${port}`,
|
|
3861
|
+
})
|
|
3862
|
+
|
|
3863
|
+
wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
3864
|
+
serverSocket = socket
|
|
3865
|
+
void TempoWs.serve({
|
|
3866
|
+
socket,
|
|
3867
|
+
store: backingStore,
|
|
3868
|
+
url: `${server.url}/ws`,
|
|
3869
|
+
route,
|
|
3870
|
+
generate: async function* (stream: TempoWs.SessionController) {
|
|
3871
|
+
await stream.charge()
|
|
3872
|
+
yield 'chunk-1'
|
|
3873
|
+
await stream.charge()
|
|
3874
|
+
yield 'chunk-2'
|
|
3875
|
+
await new Promise((resolve) => setTimeout(resolve, 60_000))
|
|
3876
|
+
},
|
|
3877
|
+
})
|
|
3878
|
+
})
|
|
3879
|
+
|
|
3880
|
+
nodeServer.on('upgrade', (req, socket, head) => {
|
|
3881
|
+
if (req.url !== '/ws') {
|
|
3882
|
+
socket.destroy()
|
|
3883
|
+
return
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
wsServer.handleUpgrade(req, socket, head, (websocket: import('ws').WebSocket) => {
|
|
3887
|
+
wsServer.emit('connection', websocket, req)
|
|
3888
|
+
})
|
|
3889
|
+
})
|
|
3890
|
+
|
|
3891
|
+
try {
|
|
3892
|
+
const manager = sessionManager({
|
|
3893
|
+
account: payer,
|
|
3894
|
+
client,
|
|
3895
|
+
escrowContract,
|
|
3896
|
+
fetch: globalThis.fetch,
|
|
3897
|
+
maxDeposit: '3',
|
|
3898
|
+
})
|
|
3899
|
+
|
|
3900
|
+
const ws = await manager.ws(`ws://localhost:${port}/ws`)
|
|
3901
|
+
const chunks: string[] = []
|
|
3902
|
+
|
|
3903
|
+
await new Promise<void>((resolve) => {
|
|
3904
|
+
ws.addEventListener('message', (event) => {
|
|
3905
|
+
if (typeof event.data !== 'string') return
|
|
3906
|
+
chunks.push(event.data)
|
|
3907
|
+
if (chunks.length === 2) serverSocket?.terminate()
|
|
3908
|
+
})
|
|
3909
|
+
ws.addEventListener('close', () => resolve(), { once: true })
|
|
3910
|
+
})
|
|
3911
|
+
|
|
3912
|
+
expect(chunks).toEqual(['chunk-1', 'chunk-2'])
|
|
3913
|
+
|
|
3914
|
+
const closeReceipt = await manager.close()
|
|
3915
|
+
expect(closeReceipt).toBeDefined()
|
|
3916
|
+
|
|
3917
|
+
const settledAmount = BigInt(closeReceipt!.spent)
|
|
3918
|
+
const deliveredCost = 2n * 1000000n
|
|
3919
|
+
expect(settledAmount).toBeLessThanOrEqual(deliveredCost)
|
|
3920
|
+
expect(settledAmount).toBeGreaterThan(0n)
|
|
3921
|
+
|
|
3922
|
+
const fullDeposit = 3000000n
|
|
3923
|
+
expect(settledAmount).toBeLessThan(fullDeposit)
|
|
3924
|
+
} finally {
|
|
3925
|
+
wsServer.close()
|
|
3926
|
+
server.close()
|
|
3927
|
+
}
|
|
3928
|
+
})
|
|
3929
|
+
|
|
3930
|
+
test('rejects tx-bearing open receipts replayed during websocket close', async () => {
|
|
3931
|
+
const routeHandler = Mppx_server.create({
|
|
3932
|
+
methods: [
|
|
3933
|
+
tempo_server.session({
|
|
3934
|
+
store: Store.memory(),
|
|
3935
|
+
getClient: () => client,
|
|
3936
|
+
account: recipientAccount,
|
|
3937
|
+
currency,
|
|
3938
|
+
escrowContract,
|
|
3939
|
+
chainId: chain.id,
|
|
3940
|
+
}),
|
|
3941
|
+
],
|
|
3942
|
+
realm: 'api.example.com',
|
|
3943
|
+
secretKey: 'secret',
|
|
3944
|
+
}).session({ amount: '1', decimals: 6, unitType: 'token' })
|
|
3945
|
+
|
|
3946
|
+
const httpHandler = NodeRequest.toNodeListener(async (request) => {
|
|
3947
|
+
const result = await routeHandler(request)
|
|
3948
|
+
if (result.status === 402) return result.challenge
|
|
3949
|
+
return result.withReceipt(new Response('ok'))
|
|
3950
|
+
})
|
|
3951
|
+
|
|
3952
|
+
const nodeServer = node_http.createServer(httpHandler)
|
|
3953
|
+
const wsServer = new WebSocketServer({ noServer: true })
|
|
3954
|
+
|
|
3955
|
+
await new Promise<void>((resolve) => nodeServer.listen(0, resolve))
|
|
3956
|
+
const { port } = nodeServer.address() as { port: number }
|
|
3957
|
+
const server = Http.wrapServer(nodeServer, {
|
|
3958
|
+
port,
|
|
3959
|
+
url: `http://localhost:${port}`,
|
|
3960
|
+
})
|
|
3961
|
+
|
|
3962
|
+
wsServer.on('connection', (socket: import('ws').WebSocket) => {
|
|
3963
|
+
let openCredential: any
|
|
3964
|
+
let openReceipt: SessionReceipt | undefined
|
|
3965
|
+
|
|
3966
|
+
socket.on('message', (data) => {
|
|
3967
|
+
const raw = data.toString()
|
|
3968
|
+
const message = TempoWs.parseMessage(raw)
|
|
3969
|
+
if (!message) return
|
|
3970
|
+
|
|
3971
|
+
if (message.mpp === 'authorization') {
|
|
3972
|
+
const credential = Credential.deserialize<any>(message.authorization)
|
|
3973
|
+
if (credential.payload?.action === 'close') {
|
|
3974
|
+
if (openReceipt) socket.send(TempoWs.formatReceiptMessage(openReceipt))
|
|
3975
|
+
return
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
openCredential = credential
|
|
3979
|
+
openReceipt = {
|
|
3980
|
+
method: 'tempo',
|
|
3981
|
+
intent: 'session',
|
|
3982
|
+
status: 'success',
|
|
3983
|
+
timestamp: new Date().toISOString(),
|
|
3984
|
+
reference: credential.payload.channelId,
|
|
3985
|
+
challengeId: credential.challenge.id,
|
|
3986
|
+
channelId: credential.payload.channelId,
|
|
3987
|
+
acceptedCumulative: credential.payload.cumulativeAmount,
|
|
3988
|
+
spent: '0',
|
|
3989
|
+
units: 0,
|
|
3990
|
+
txHash: `0x${'12'.repeat(32)}` as Hex,
|
|
3991
|
+
}
|
|
3992
|
+
socket.send(TempoWs.formatReceiptMessage(openReceipt))
|
|
3993
|
+
return
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
if (message.mpp === 'payment-close-request' && openCredential) {
|
|
3997
|
+
socket.send(
|
|
3998
|
+
TempoWs.formatCloseReadyMessage({
|
|
3999
|
+
method: 'tempo',
|
|
4000
|
+
intent: 'session',
|
|
4001
|
+
status: 'success',
|
|
4002
|
+
timestamp: new Date().toISOString(),
|
|
4003
|
+
reference: openCredential.payload.channelId,
|
|
4004
|
+
challengeId: openCredential.challenge.id,
|
|
4005
|
+
channelId: openCredential.payload.channelId,
|
|
4006
|
+
acceptedCumulative: openCredential.payload.cumulativeAmount,
|
|
4007
|
+
spent: '0',
|
|
4008
|
+
units: 0,
|
|
4009
|
+
}),
|
|
4010
|
+
)
|
|
4011
|
+
}
|
|
4012
|
+
})
|
|
4013
|
+
})
|
|
4014
|
+
|
|
4015
|
+
nodeServer.on('upgrade', (req, socket, head) => {
|
|
4016
|
+
if (req.url !== '/ws') {
|
|
4017
|
+
socket.destroy()
|
|
4018
|
+
return
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
wsServer.handleUpgrade(req, socket, head, (websocket: import('ws').WebSocket) => {
|
|
4022
|
+
wsServer.emit('connection', websocket, req)
|
|
4023
|
+
})
|
|
4024
|
+
})
|
|
4025
|
+
|
|
4026
|
+
try {
|
|
4027
|
+
const manager = sessionManager({
|
|
4028
|
+
account: payer,
|
|
4029
|
+
client,
|
|
4030
|
+
escrowContract,
|
|
4031
|
+
fetch: globalThis.fetch,
|
|
4032
|
+
maxDeposit: '1',
|
|
4033
|
+
})
|
|
4034
|
+
|
|
4035
|
+
await manager.ws(`ws://localhost:${port}/ws`)
|
|
4036
|
+
await expect(manager.close()).rejects.toThrow(
|
|
4037
|
+
'received mismatched payment-close receipt frame',
|
|
4038
|
+
)
|
|
4039
|
+
} finally {
|
|
4040
|
+
wsServer.close()
|
|
4041
|
+
server.close()
|
|
4042
|
+
}
|
|
4043
|
+
})
|
|
4044
|
+
})
|
|
3080
4045
|
})
|
|
3081
4046
|
|
|
3082
4047
|
describe('monotonicity and TOCTOU (unit tests)', () => {
|
|
@@ -3113,7 +4078,11 @@ describe('monotonicity and TOCTOU (unit tests)', () => {
|
|
|
3113
4078
|
|
|
3114
4079
|
test('charge does not allow highestVoucherAmount to decrease', async () => {
|
|
3115
4080
|
const store = ChannelStore.fromStore(Store.memory())
|
|
3116
|
-
await seedChannel(store, {
|
|
4081
|
+
await seedChannel(store, {
|
|
4082
|
+
highestVoucherAmount: 5000000n,
|
|
4083
|
+
spent: 0n,
|
|
4084
|
+
units: 0,
|
|
4085
|
+
})
|
|
3117
4086
|
|
|
3118
4087
|
const channel = await charge(store, testChannelId, 1000000n)
|
|
3119
4088
|
expect(channel.spent).toBe(1000000n)
|
|
@@ -3154,7 +4123,11 @@ describe('monotonicity and TOCTOU (unit tests)', () => {
|
|
|
3154
4123
|
|
|
3155
4124
|
test('acceptVoucher is monotonic — lower value does not decrease highestVoucherAmount', async () => {
|
|
3156
4125
|
const store = ChannelStore.fromStore(Store.memory())
|
|
3157
|
-
await seedChannel(store, {
|
|
4126
|
+
await seedChannel(store, {
|
|
4127
|
+
highestVoucherAmount: 5000000n,
|
|
4128
|
+
spent: 2000000n,
|
|
4129
|
+
units: 3,
|
|
4130
|
+
})
|
|
3158
4131
|
|
|
3159
4132
|
const channel = await store.updateChannel(testChannelId, (existing) => {
|
|
3160
4133
|
if (!existing) return null
|
|
@@ -3579,7 +4552,9 @@ async function createSignedOpenTransaction(
|
|
|
3579
4552
|
token: currency,
|
|
3580
4553
|
deposit,
|
|
3581
4554
|
salt,
|
|
3582
|
-
...(opts?.authorizedSigner !== undefined && {
|
|
4555
|
+
...(opts?.authorizedSigner !== undefined && {
|
|
4556
|
+
authorizedSigner: opts.authorizedSigner,
|
|
4557
|
+
}),
|
|
3583
4558
|
})
|
|
3584
4559
|
return { channelId, serializedTransaction }
|
|
3585
4560
|
}
|
|
@@ -3595,36 +4570,58 @@ function mutateSignature(signature: Hex): Hex {
|
|
|
3595
4570
|
return `${signature.slice(0, -1)}${replacement}` as Hex
|
|
3596
4571
|
}
|
|
3597
4572
|
|
|
3598
|
-
function withFaultHooks(store: Store.
|
|
3599
|
-
let
|
|
3600
|
-
|
|
4573
|
+
function withFaultHooks(store: Store.AtomicStore, options: { failPutAt: number }) {
|
|
4574
|
+
let writeCalls = 0
|
|
4575
|
+
|
|
4576
|
+
function maybeFail(key: string) {
|
|
4577
|
+
writeCalls++
|
|
4578
|
+
if (writeCalls === options.failPutAt)
|
|
4579
|
+
throw new Error(`simulated store crash before persisting key ${key}`)
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4582
|
+
return Store.from<Store.AtomicStore>({
|
|
3601
4583
|
get: (key) => store.get(key),
|
|
3602
|
-
delete: (key) =>
|
|
4584
|
+
delete: async (key) => {
|
|
4585
|
+
maybeFail(key)
|
|
4586
|
+
await store.delete(key)
|
|
4587
|
+
},
|
|
3603
4588
|
put: async (key, value) => {
|
|
3604
|
-
|
|
3605
|
-
if (putCalls === options.failPutAt)
|
|
3606
|
-
throw new Error(`simulated store crash before persisting key ${key}`)
|
|
4589
|
+
maybeFail(key)
|
|
3607
4590
|
await store.put(key, value)
|
|
3608
4591
|
},
|
|
4592
|
+
update(key, fn) {
|
|
4593
|
+
return store.update(key, (current) => {
|
|
4594
|
+
const change = fn(current)
|
|
4595
|
+
if (change.op !== 'noop') maybeFail(key)
|
|
4596
|
+
return change
|
|
4597
|
+
})
|
|
4598
|
+
},
|
|
3609
4599
|
})
|
|
3610
4600
|
}
|
|
3611
4601
|
|
|
3612
|
-
function withReadDropHooks(store: Store.
|
|
4602
|
+
function withReadDropHooks(store: Store.AtomicStore) {
|
|
3613
4603
|
const pending = new Map<string, number>()
|
|
3614
|
-
|
|
4604
|
+
|
|
4605
|
+
function readOrDrop<value>(key: string, current: value | null) {
|
|
4606
|
+
const remaining = pending.get(key)
|
|
4607
|
+
if (remaining === undefined) return current
|
|
4608
|
+
if (remaining === 0) {
|
|
4609
|
+
pending.delete(key)
|
|
4610
|
+
return null
|
|
4611
|
+
}
|
|
4612
|
+
pending.set(key, remaining - 1)
|
|
4613
|
+
return current
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
const wrapped = Store.from<Store.AtomicStore>({
|
|
3615
4617
|
async get(key) {
|
|
3616
|
-
|
|
3617
|
-
if (remaining !== undefined) {
|
|
3618
|
-
if (remaining === 0) {
|
|
3619
|
-
pending.delete(key)
|
|
3620
|
-
return null
|
|
3621
|
-
}
|
|
3622
|
-
pending.set(key, remaining - 1)
|
|
3623
|
-
}
|
|
3624
|
-
return store.get(key)
|
|
4618
|
+
return readOrDrop(key, await store.get(key))
|
|
3625
4619
|
},
|
|
3626
4620
|
put: (key, value) => store.put(key, value),
|
|
3627
4621
|
delete: (key) => store.delete(key),
|
|
4622
|
+
update(key, fn) {
|
|
4623
|
+
return store.update(key, (current) => fn(readOrDrop(key, current)))
|
|
4624
|
+
},
|
|
3628
4625
|
})
|
|
3629
4626
|
return {
|
|
3630
4627
|
store: wrapped,
|