mppx 0.3.5 → 0.3.7

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.
Files changed (61) hide show
  1. package/README.md +5 -4
  2. package/dist/BodyDigest.d.ts.map +1 -1
  3. package/dist/BodyDigest.js +2 -1
  4. package/dist/BodyDigest.js.map +1 -1
  5. package/dist/Challenge.d.ts.map +1 -1
  6. package/dist/Challenge.js +1 -9
  7. package/dist/Challenge.js.map +1 -1
  8. package/dist/internal/constantTimeEqual.d.ts +3 -0
  9. package/dist/internal/constantTimeEqual.d.ts.map +1 -0
  10. package/dist/internal/constantTimeEqual.js +10 -0
  11. package/dist/internal/constantTimeEqual.js.map +1 -0
  12. package/dist/internal/types.d.ts +10 -0
  13. package/dist/internal/types.d.ts.map +1 -1
  14. package/dist/proxy/internal/Headers.d.ts +2 -0
  15. package/dist/proxy/internal/Headers.d.ts.map +1 -1
  16. package/dist/proxy/internal/Headers.js +2 -0
  17. package/dist/proxy/internal/Headers.js.map +1 -1
  18. package/dist/proxy/internal/Route.d.ts +4 -0
  19. package/dist/proxy/internal/Route.d.ts.map +1 -1
  20. package/dist/proxy/internal/Route.js +4 -0
  21. package/dist/proxy/internal/Route.js.map +1 -1
  22. package/dist/server/NodeListener.d.ts +6 -0
  23. package/dist/server/NodeListener.d.ts.map +1 -1
  24. package/dist/server/NodeListener.js +6 -0
  25. package/dist/server/NodeListener.js.map +1 -1
  26. package/dist/server/Response.d.ts +17 -0
  27. package/dist/server/Response.d.ts.map +1 -1
  28. package/dist/server/Response.js +17 -0
  29. package/dist/server/Response.js.map +1 -1
  30. package/dist/tempo/client/ChannelOps.js.map +1 -1
  31. package/dist/tempo/client/Charge.d.ts.map +1 -1
  32. package/dist/tempo/client/Charge.js +1 -0
  33. package/dist/tempo/client/Charge.js.map +1 -1
  34. package/dist/tempo/internal/defaults.d.ts +34 -8
  35. package/dist/tempo/internal/defaults.d.ts.map +1 -1
  36. package/dist/tempo/internal/defaults.js +30 -8
  37. package/dist/tempo/internal/defaults.js.map +1 -1
  38. package/dist/tempo/server/Charge.js +2 -2
  39. package/dist/tempo/server/Charge.js.map +1 -1
  40. package/dist/tempo/server/Session.d.ts.map +1 -1
  41. package/dist/tempo/server/Session.js +8 -3
  42. package/dist/tempo/server/Session.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/BodyDigest.ts +2 -1
  45. package/src/Challenge.ts +1 -8
  46. package/src/internal/constantTimeEqual.test.ts +46 -0
  47. package/src/internal/constantTimeEqual.ts +7 -0
  48. package/src/internal/types.ts +11 -0
  49. package/src/proxy/internal/Headers.ts +2 -0
  50. package/src/proxy/internal/Route.ts +4 -0
  51. package/src/server/NodeListener.ts +6 -0
  52. package/src/server/Response.ts +17 -0
  53. package/src/tempo/client/ChannelOps.ts +1 -1
  54. package/src/tempo/client/Charge.ts +1 -0
  55. package/src/tempo/internal/defaults.test.ts +94 -0
  56. package/src/tempo/internal/defaults.ts +41 -8
  57. package/src/tempo/server/Charge.test.ts +150 -0
  58. package/src/tempo/server/Charge.ts +2 -2
  59. package/src/tempo/server/Session.test.ts +189 -1
  60. package/src/tempo/server/Session.ts +8 -3
  61. package/src/tempo/session/Voucher.test.ts +46 -0
@@ -656,6 +656,156 @@ describe('tempo', () => {
656
656
  })
657
657
  })
658
658
 
659
+ describe('default currency resolution', () => {
660
+ test('mainnet (default) resolves to USDC', () => {
661
+ const method = tempo_server.charge({
662
+ getClient: () => client,
663
+ account: accounts[0].address,
664
+ })
665
+ expect((method.defaults as Record<string, unknown>)?.currency).toBe(
666
+ '0x20C000000000000000000000b9537d11c60E8b50',
667
+ )
668
+ })
669
+
670
+ test('testnet: true defaults to pathUSD', () => {
671
+ const method = tempo_server.charge({
672
+ getClient: () => client,
673
+ account: accounts[0].address,
674
+ testnet: true,
675
+ })
676
+ expect((method.defaults as Record<string, unknown>)?.currency).toBe(
677
+ '0x20c0000000000000000000000000000000000000',
678
+ )
679
+ })
680
+
681
+ test('unknown chain defaults to pathUSD', () => {
682
+ const method = tempo_server.charge({
683
+ getClient: () => client,
684
+ account: accounts[0].address,
685
+ chainId: 69420,
686
+ })
687
+ expect((method.defaults as Record<string, unknown>)?.currency).toBe(
688
+ '0x20c0000000000000000000000000000000000000',
689
+ )
690
+ })
691
+
692
+ test('explicit currency overrides default', () => {
693
+ const method = tempo_server.charge({
694
+ getClient: () => client,
695
+ account: accounts[0].address,
696
+ testnet: false,
697
+ currency: '0xcustom',
698
+ })
699
+ expect(method.defaults?.currency).toBe('0xcustom')
700
+ })
701
+
702
+ test('decimals defaults to 6', () => {
703
+ const method = tempo_server.charge({
704
+ getClient: () => client,
705
+ account: accounts[0].address,
706
+ })
707
+ expect((method.defaults as Record<string, unknown>)?.decimals).toBe(6)
708
+ })
709
+
710
+ test('challenge contains USDC currency (mainnet default)', async () => {
711
+ const handler = Mppx_server.create({
712
+ methods: [
713
+ tempo_server.charge({
714
+ getClient: () => client,
715
+ account: accounts[0].address,
716
+ }),
717
+ ],
718
+ realm,
719
+ secretKey,
720
+ })
721
+
722
+ const result = await (handler.charge as Function)({ amount: '1' })(
723
+ new Request('https://example.com'),
724
+ )
725
+ expect(result.status).toBe(402)
726
+ if (result.status !== 402) throw new Error()
727
+
728
+ const challenge = Challenge.fromResponse(result.challenge, {
729
+ methods: [tempo_client.charge()],
730
+ })
731
+ expect(challenge.request.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
732
+ })
733
+
734
+ test('challenge contains pathUSD currency when testnet: true', async () => {
735
+ const handler = Mppx_server.create({
736
+ methods: [
737
+ tempo_server.charge({
738
+ getClient: () => client,
739
+ account: accounts[0].address,
740
+ testnet: true,
741
+ }),
742
+ ],
743
+ realm,
744
+ secretKey,
745
+ })
746
+
747
+ const result = await (handler.charge as Function)({ amount: '1', chainId: chain.id })(
748
+ new Request('https://example.com'),
749
+ )
750
+ expect(result.status).toBe(402)
751
+ if (result.status !== 402) throw new Error()
752
+
753
+ const challenge = Challenge.fromResponse(result.challenge, {
754
+ methods: [tempo_client.charge()],
755
+ })
756
+ expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
757
+ })
758
+
759
+ test('challenge contains pathUSD currency (unknown chain)', async () => {
760
+ const handler = Mppx_server.create({
761
+ methods: [
762
+ tempo_server.charge({
763
+ getClient: () => client,
764
+ account: accounts[0].address,
765
+ chainId: 69420,
766
+ }),
767
+ ],
768
+ realm,
769
+ secretKey,
770
+ })
771
+
772
+ const result = await (handler.charge as Function)({ amount: '1' })(
773
+ new Request('https://example.com'),
774
+ )
775
+ expect(result.status).toBe(402)
776
+ if (result.status !== 402) throw new Error()
777
+
778
+ const challenge = Challenge.fromResponse(result.challenge, {
779
+ methods: [tempo_client.charge()],
780
+ })
781
+ expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
782
+ })
783
+
784
+ test('explicit currency in challenge overrides testnet default', async () => {
785
+ const handler = Mppx_server.create({
786
+ methods: [
787
+ tempo_server.charge({
788
+ getClient: () => client,
789
+ account: accounts[0].address,
790
+ testnet: false,
791
+ currency: asset,
792
+ }),
793
+ ],
794
+ realm,
795
+ secretKey,
796
+ })
797
+
798
+ const result = await handler.charge({ amount: '1' })(new Request('https://example.com'))
799
+ expect(result.status).toBe(402)
800
+ if (result.status !== 402) throw new Error()
801
+
802
+ const challenge = Challenge.fromResponse(result.challenge, {
803
+ methods: [tempo_client.charge()],
804
+ })
805
+ expect(challenge.request.currency).toBe(asset)
806
+ })
807
+ })
808
+
659
809
  describe('attribution memo', () => {
660
810
  test('client always generates attribution memo (hash credential)', async () => {
661
811
  const httpServer = await Http.createServer(async (req, res) => {
@@ -40,7 +40,7 @@ export function charge<const parameters extends charge.Parameters>(
40
40
  ) {
41
41
  const {
42
42
  amount,
43
- currency,
43
+ currency = defaults.resolveCurrency(parameters),
44
44
  decimals = defaults.decimals,
45
45
  description,
46
46
  externalId,
@@ -71,7 +71,7 @@ export function charge<const parameters extends charge.Parameters>(
71
71
  async request({ credential, request }) {
72
72
  const chainId = await (async () => {
73
73
  if (request.chainId) return request.chainId
74
- if (parameters.testnet) return defaults.testnetChainId
74
+ if (parameters.testnet) return defaults.chainId.testnet
75
75
  return (await getClient({})).chain?.id
76
76
  })()
77
77
 
@@ -1,4 +1,5 @@
1
- import type { Challenge, z } from 'mppx'
1
+ import type { z } from 'mppx'
2
+ import { Challenge } from 'mppx'
2
3
  import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
3
4
  import { type Address, createClient, type Hex } from 'viem'
4
5
  import { Addresses } from 'viem/tempo'
@@ -1334,6 +1335,193 @@ describe('monotonicity and TOCTOU (unit tests)', () => {
1334
1335
  })
1335
1336
  })
1336
1337
 
1338
+ describe('session default currency resolution', () => {
1339
+ const mockClient = createClient({ transport: http('http://localhost:1') })
1340
+ const mockMainnetClient = createClient({
1341
+ chain: {
1342
+ id: 4217,
1343
+ name: 'Tempo',
1344
+ nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
1345
+ rpcUrls: { default: { http: ['http://localhost:1'] } },
1346
+ },
1347
+ transport: http('http://localhost:1'),
1348
+ })
1349
+ const mockTestnetClient = createClient({
1350
+ chain: {
1351
+ id: 42431,
1352
+ name: 'Tempo Testnet',
1353
+ nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
1354
+ rpcUrls: { default: { http: ['http://localhost:1'] } },
1355
+ },
1356
+ transport: http('http://localhost:1'),
1357
+ })
1358
+
1359
+ test('mainnet (default) resolves to USDC', () => {
1360
+ const server = session({
1361
+ store: Store.memory(),
1362
+ getClient: () => mockClient,
1363
+ account: '0x0000000000000000000000000000000000000001',
1364
+ escrowContract: '0x0000000000000000000000000000000000000002',
1365
+ } as session.Parameters)
1366
+ expect(server.defaults?.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
1367
+ })
1368
+
1369
+ test('testnet: true defaults to pathUSD', () => {
1370
+ const server = session({
1371
+ store: Store.memory(),
1372
+ getClient: () => mockClient,
1373
+ account: '0x0000000000000000000000000000000000000001',
1374
+ escrowContract: '0x0000000000000000000000000000000000000002',
1375
+ testnet: true,
1376
+ } as session.Parameters)
1377
+ expect(server.defaults?.currency).toBe('0x20c0000000000000000000000000000000000000')
1378
+ })
1379
+
1380
+ test('unknown chain defaults to pathUSD', () => {
1381
+ const server = session({
1382
+ store: Store.memory(),
1383
+ getClient: () => mockClient,
1384
+ account: '0x0000000000000000000000000000000000000001',
1385
+ escrowContract: '0x0000000000000000000000000000000000000002',
1386
+ chainId: 69420,
1387
+ } as session.Parameters)
1388
+ expect(server.defaults?.currency).toBe('0x20c0000000000000000000000000000000000000')
1389
+ })
1390
+
1391
+ test('explicit currency overrides default', () => {
1392
+ const server = session({
1393
+ store: Store.memory(),
1394
+ getClient: () => mockClient,
1395
+ account: '0x0000000000000000000000000000000000000001',
1396
+ currency: '0xcustom',
1397
+ escrowContract: '0x0000000000000000000000000000000000000002',
1398
+ chainId: 4217,
1399
+ testnet: false,
1400
+ } as session.Parameters)
1401
+ expect(server.defaults?.currency).toBe('0xcustom')
1402
+ })
1403
+
1404
+ test('decimals defaults to 6', () => {
1405
+ const server = session({
1406
+ store: Store.memory(),
1407
+ getClient: () => mockClient,
1408
+ account: '0x0000000000000000000000000000000000000001',
1409
+ escrowContract: '0x0000000000000000000000000000000000000002',
1410
+ chainId: 42431,
1411
+ } as session.Parameters)
1412
+ expect(server.defaults?.decimals).toBe(6)
1413
+ })
1414
+
1415
+ test('challenge contains USDC currency (mainnet default)', async () => {
1416
+ const handler = Mppx_server.create({
1417
+ methods: [
1418
+ tempo_server.session({
1419
+ store: Store.memory(),
1420
+ getClient: () => mockMainnetClient,
1421
+ account: '0x0000000000000000000000000000000000000001',
1422
+ escrowContract: '0x0000000000000000000000000000000000000002',
1423
+ chainId: 4217,
1424
+ testnet: false,
1425
+ }),
1426
+ ],
1427
+ realm: 'api.example.com',
1428
+ secretKey: 'secret',
1429
+ })
1430
+
1431
+ const result = await (handler.session as Function)({
1432
+ amount: '1',
1433
+ decimals: 6,
1434
+ unitType: 'token',
1435
+ })(new Request('https://example.com'))
1436
+ expect(result.status).toBe(402)
1437
+
1438
+ const challenge = Challenge.fromResponse(result.challenge)
1439
+ expect(challenge.request.currency).toBe('0x20C000000000000000000000b9537d11c60E8b50')
1440
+ })
1441
+
1442
+ test('challenge contains pathUSD currency when testnet: true', async () => {
1443
+ const handler = Mppx_server.create({
1444
+ methods: [
1445
+ tempo_server.session({
1446
+ store: Store.memory(),
1447
+ getClient: () => mockTestnetClient,
1448
+ account: '0x0000000000000000000000000000000000000001',
1449
+ escrowContract: '0x0000000000000000000000000000000000000002',
1450
+ testnet: true,
1451
+ }),
1452
+ ],
1453
+ realm: 'api.example.com',
1454
+ secretKey: 'secret',
1455
+ })
1456
+
1457
+ const result = await (handler.session as Function)({
1458
+ amount: '1',
1459
+ decimals: 6,
1460
+ unitType: 'token',
1461
+ chainId: 42431,
1462
+ })(new Request('https://example.com'))
1463
+ expect(result.status).toBe(402)
1464
+
1465
+ const challenge = Challenge.fromResponse(result.challenge)
1466
+ expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
1467
+ })
1468
+
1469
+ test('challenge contains pathUSD currency (unknown chain)', async () => {
1470
+ const handler = Mppx_server.create({
1471
+ methods: [
1472
+ tempo_server.session({
1473
+ store: Store.memory(),
1474
+ getClient: () => mockTestnetClient,
1475
+ account: '0x0000000000000000000000000000000000000001',
1476
+ escrowContract: '0x0000000000000000000000000000000000000002',
1477
+ chainId: 69420,
1478
+ }),
1479
+ ],
1480
+ realm: 'api.example.com',
1481
+ secretKey: 'secret',
1482
+ })
1483
+
1484
+ const result = await (handler.session as Function)({
1485
+ amount: '1',
1486
+ decimals: 6,
1487
+ unitType: 'token',
1488
+ })(new Request('https://example.com'))
1489
+ expect(result.status).toBe(402)
1490
+
1491
+ const challenge = Challenge.fromResponse(result.challenge)
1492
+ expect(challenge.request.currency).toBe('0x20c0000000000000000000000000000000000000')
1493
+ })
1494
+
1495
+ test('explicit currency in challenge overrides testnet default', async () => {
1496
+ const handler = Mppx_server.create({
1497
+ methods: [
1498
+ tempo_server.session({
1499
+ store: Store.memory(),
1500
+ getClient: () => mockClient,
1501
+ account: '0x0000000000000000000000000000000000000001',
1502
+ currency: '0xcustom',
1503
+ escrowContract: '0x0000000000000000000000000000000000000002',
1504
+ chainId: 4217,
1505
+ testnet: false,
1506
+ }),
1507
+ ],
1508
+ realm: 'api.example.com',
1509
+ secretKey: 'secret',
1510
+ })
1511
+
1512
+ const result = await handler.session({
1513
+ amount: '1',
1514
+ decimals: 6,
1515
+ unitType: 'token',
1516
+ })(new Request('https://example.com'))
1517
+ expect(result.status).toBe(402)
1518
+ if (result.status !== 402) throw new Error()
1519
+
1520
+ const challenge = Challenge.fromResponse(result.challenge)
1521
+ expect(challenge.request.currency).toBe('0xcustom')
1522
+ })
1523
+ })
1524
+
1337
1525
  function nextSalt(): Hex {
1338
1526
  saltCounter++
1339
1527
  return `0x${saltCounter.toString(16).padStart(64, '0')}` as Hex
@@ -85,7 +85,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
85
85
  const parameters = p as parameters
86
86
  const {
87
87
  amount,
88
- currency,
88
+ currency = defaults.resolveCurrency(parameters),
89
89
  decimals = defaults.decimals,
90
90
  store: rawStore = Store.memory(),
91
91
  suggestedDeposit,
@@ -127,7 +127,7 @@ export function session<const parameters extends session.Parameters>(p?: paramet
127
127
  // Extract chainId from request or default.
128
128
  const chainId = await (async () => {
129
129
  if (request.chainId) return request.chainId
130
- if (parameters.testnet) return defaults.testnetChainId
130
+ if (parameters.testnet) return defaults.chainId.testnet
131
131
  return (await getClient({})).chain?.id
132
132
  })()
133
133
 
@@ -156,7 +156,12 @@ export function session<const parameters extends session.Parameters>(p?: paramet
156
156
  return undefined
157
157
  })()
158
158
 
159
- return { ...request, chainId, escrowContract: resolvedEscrow, feePayer: resolvedFeePayer }
159
+ return {
160
+ ...request,
161
+ chainId,
162
+ escrowContract: resolvedEscrow,
163
+ feePayer: resolvedFeePayer,
164
+ }
160
165
  },
161
166
 
162
167
  async verify({ credential }) {
@@ -131,4 +131,50 @@ describe('Voucher', () => {
131
131
  expect(voucher.cumulativeAmount).toBe(5000000n)
132
132
  expect(voucher.signature).toBe(sig)
133
133
  })
134
+
135
+ test('parseVoucherFromPayload with zero amount', () => {
136
+ const sig =
137
+ '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab' as const
138
+ const voucher = parseVoucherFromPayload(channelId, '0', sig)
139
+ expect(voucher.cumulativeAmount).toBe(0n)
140
+ })
141
+
142
+ test('verifyVoucher rejects wrong escrow contract', async () => {
143
+ const signature = await signVoucher(
144
+ client,
145
+ account,
146
+ { channelId, cumulativeAmount },
147
+ escrowContract,
148
+ chainId,
149
+ )
150
+
151
+ const wrongEscrow = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd' as const
152
+ const isValid = await verifyVoucher(
153
+ wrongEscrow,
154
+ chainId,
155
+ { channelId, cumulativeAmount, signature },
156
+ account.address,
157
+ )
158
+ expect(isValid).toBe(false)
159
+ })
160
+
161
+ test('signVoucher and verifyVoucher round-trip with zero amount', async () => {
162
+ const zeroAmount = 0n
163
+ const signature = await signVoucher(
164
+ client,
165
+ account,
166
+ { channelId, cumulativeAmount: zeroAmount },
167
+ escrowContract,
168
+ chainId,
169
+ )
170
+ expect(signature).toMatch(/^0x/)
171
+
172
+ const isValid = await verifyVoucher(
173
+ escrowContract,
174
+ chainId,
175
+ { channelId, cumulativeAmount: zeroAmount, signature },
176
+ account.address,
177
+ )
178
+ expect(isValid).toBe(true)
179
+ })
134
180
  })