mppx 0.4.11 → 0.5.0

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 (89) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/Expires.d.ts +7 -0
  3. package/dist/Expires.d.ts.map +1 -1
  4. package/dist/Expires.js +21 -0
  5. package/dist/Expires.js.map +1 -1
  6. package/dist/internal/env.d.ts +1 -1
  7. package/dist/internal/env.d.ts.map +1 -1
  8. package/dist/internal/env.js +2 -6
  9. package/dist/internal/env.js.map +1 -1
  10. package/dist/internal/types.d.ts +23 -0
  11. package/dist/internal/types.d.ts.map +1 -1
  12. package/dist/server/Mppx.d.ts +1 -1
  13. package/dist/server/Mppx.d.ts.map +1 -1
  14. package/dist/server/Mppx.js +55 -7
  15. package/dist/server/Mppx.js.map +1 -1
  16. package/dist/stripe/server/Charge.d.ts.map +1 -1
  17. package/dist/stripe/server/Charge.js +3 -3
  18. package/dist/stripe/server/Charge.js.map +1 -1
  19. package/dist/tempo/Methods.d.ts +18 -0
  20. package/dist/tempo/Methods.d.ts.map +1 -1
  21. package/dist/tempo/Methods.js +28 -3
  22. package/dist/tempo/Methods.js.map +1 -1
  23. package/dist/tempo/client/Charge.d.ts +24 -0
  24. package/dist/tempo/client/Charge.d.ts.map +1 -1
  25. package/dist/tempo/client/Charge.js +51 -9
  26. package/dist/tempo/client/Charge.js.map +1 -1
  27. package/dist/tempo/client/Methods.d.ts +18 -0
  28. package/dist/tempo/client/Methods.d.ts.map +1 -1
  29. package/dist/tempo/internal/account.d.ts +5 -11
  30. package/dist/tempo/internal/account.d.ts.map +1 -1
  31. package/dist/tempo/internal/charge.d.ts +20 -0
  32. package/dist/tempo/internal/charge.d.ts.map +1 -0
  33. package/dist/tempo/internal/charge.js +23 -0
  34. package/dist/tempo/internal/charge.js.map +1 -0
  35. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  36. package/dist/tempo/internal/fee-payer.js +15 -3
  37. package/dist/tempo/internal/fee-payer.js.map +1 -1
  38. package/dist/tempo/internal/proof.d.ts +23 -0
  39. package/dist/tempo/internal/proof.d.ts.map +1 -0
  40. package/dist/tempo/internal/proof.js +17 -0
  41. package/dist/tempo/internal/proof.js.map +1 -0
  42. package/dist/tempo/server/Charge.d.ts +20 -2
  43. package/dist/tempo/server/Charge.d.ts.map +1 -1
  44. package/dist/tempo/server/Charge.js +180 -103
  45. package/dist/tempo/server/Charge.js.map +1 -1
  46. package/dist/tempo/server/Methods.d.ts +20 -2
  47. package/dist/tempo/server/Methods.d.ts.map +1 -1
  48. package/dist/tempo/server/Methods.js +4 -1
  49. package/dist/tempo/server/Methods.js.map +1 -1
  50. package/dist/tempo/server/Session.d.ts +9 -4
  51. package/dist/tempo/server/Session.d.ts.map +1 -1
  52. package/dist/tempo/server/Session.js +18 -3
  53. package/dist/tempo/server/Session.js.map +1 -1
  54. package/dist/tempo/session/Chain.d.ts +18 -2
  55. package/dist/tempo/session/Chain.d.ts.map +1 -1
  56. package/dist/tempo/session/Chain.js +18 -14
  57. package/dist/tempo/session/Chain.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/Expires.ts +25 -0
  60. package/src/cli/cli.test.ts +230 -1
  61. package/src/internal/env.test.ts +12 -12
  62. package/src/internal/env.ts +2 -6
  63. package/src/internal/types.ts +25 -0
  64. package/src/middlewares/elysia.test.ts +127 -4
  65. package/src/middlewares/express.test.ts +120 -54
  66. package/src/middlewares/hono.test.ts +73 -34
  67. package/src/middlewares/nextjs.test.ts +159 -36
  68. package/src/server/Mppx.test.ts +373 -0
  69. package/src/server/Mppx.ts +64 -10
  70. package/src/stripe/server/Charge.ts +3 -7
  71. package/src/tempo/Methods.test.ts +105 -0
  72. package/src/tempo/Methods.ts +54 -17
  73. package/src/tempo/client/Charge.ts +67 -11
  74. package/src/tempo/internal/account.ts +7 -14
  75. package/src/tempo/internal/charge.test.ts +66 -0
  76. package/src/tempo/internal/charge.ts +43 -0
  77. package/src/tempo/internal/fee-payer.test.ts +33 -14
  78. package/src/tempo/internal/fee-payer.ts +21 -6
  79. package/src/tempo/internal/proof.test.ts +36 -0
  80. package/src/tempo/internal/proof.ts +19 -0
  81. package/src/tempo/server/Charge.test.ts +593 -1
  82. package/src/tempo/server/Charge.ts +233 -126
  83. package/src/tempo/server/Methods.ts +4 -1
  84. package/src/tempo/server/Session.test.ts +1152 -54
  85. package/src/tempo/server/Session.ts +26 -17
  86. package/src/tempo/server/internal/transport.test.ts +32 -0
  87. package/src/tempo/session/Chain.test.ts +60 -5
  88. package/src/tempo/session/Chain.ts +30 -14
  89. package/src/tempo/session/Sse.test.ts +31 -0
@@ -5,7 +5,12 @@ import type { Hex } from 'ox'
5
5
  import { TxEnvelopeTempo } from 'ox/tempo'
6
6
  import { Handler } from 'tempo.ts/server'
7
7
  import { createClient, custom, encodeFunctionData, parseUnits } from 'viem'
8
- import { getTransactionReceipt, prepareTransactionRequest, signTransaction } from 'viem/actions'
8
+ import {
9
+ getTransactionReceipt,
10
+ prepareTransactionRequest,
11
+ signTypedData,
12
+ signTransaction,
13
+ } from 'viem/actions'
9
14
  import { Abis, Account, Actions, Addresses, Secp256k1, Tick, Transaction } from 'viem/tempo'
10
15
  import { beforeAll, describe, expect, test } from 'vp/test'
11
16
  import * as Http from '~test/Http.js'
@@ -14,6 +19,7 @@ import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js
14
19
 
15
20
  import * as Store from '../../Store.js'
16
21
  import * as Attribution from '../Attribution.js'
22
+ import * as Proof from '../internal/proof.js'
17
23
  import { signVoucher } from '../session/Voucher.js'
18
24
 
19
25
  const realm = 'api.example.com'
@@ -650,6 +656,107 @@ describe('tempo', () => {
650
656
  httpServer.close()
651
657
  })
652
658
 
659
+ test('behavior: accepts split payments', async () => {
660
+ const mppx = Mppx_client.create({
661
+ polyfill: false,
662
+ methods: [
663
+ tempo_client({
664
+ account: accounts[1],
665
+ getClient: () => client,
666
+ }),
667
+ ],
668
+ })
669
+
670
+ const httpServer = await Http.createServer(async (req, res) => {
671
+ const result = await Mppx_server.toNodeListener(
672
+ server.charge({
673
+ amount: '1',
674
+ currency: asset,
675
+ recipient: accounts[0].address,
676
+ splits: [
677
+ { amount: '0.2', recipient: accounts[2].address },
678
+ {
679
+ amount: '0.1',
680
+ memo: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
681
+ recipient: accounts[3].address,
682
+ },
683
+ ],
684
+ }),
685
+ )(req, res)
686
+ if (result.status === 402) return
687
+ res.end('OK')
688
+ })
689
+
690
+ const response = await mppx.fetch(httpServer.url)
691
+ expect(response.status).toBe(200)
692
+
693
+ httpServer.close()
694
+ })
695
+
696
+ test('behavior: accepts transaction when split transfers are out of order', async () => {
697
+ const httpServer = await Http.createServer(async (req, res) => {
698
+ const result = await Mppx_server.toNodeListener(
699
+ server.charge({
700
+ amount: '1',
701
+ currency: asset,
702
+ recipient: accounts[0].address,
703
+ splits: [
704
+ { amount: '0.2', recipient: accounts[2].address },
705
+ { amount: '0.1', recipient: accounts[3].address },
706
+ ],
707
+ }),
708
+ )(req, res)
709
+ if (result.status === 402) return
710
+ res.end('OK')
711
+ })
712
+
713
+ const response = await fetch(httpServer.url)
714
+ expect(response.status).toBe(402)
715
+
716
+ const challenge = Challenge.fromResponse(response, {
717
+ methods: [tempo_client.charge()],
718
+ })
719
+ const splits = challenge.request.methodDetails?.splits ?? []
720
+ const primaryAmount =
721
+ BigInt(challenge.request.amount) - BigInt(splits[0]!.amount) - BigInt(splits[1]!.amount)
722
+
723
+ const prepared = await prepareTransactionRequest(client, {
724
+ account: accounts[1]!,
725
+ calls: [
726
+ Actions.token.transfer.call({
727
+ amount: BigInt(splits[1]!.amount),
728
+ to: splits[1]!.recipient as Hex.Hex,
729
+ token: challenge.request.currency as Hex.Hex,
730
+ }),
731
+ Actions.token.transfer.call({
732
+ amount: primaryAmount,
733
+ to: challenge.request.recipient as Hex.Hex,
734
+ token: challenge.request.currency as Hex.Hex,
735
+ }),
736
+ Actions.token.transfer.call({
737
+ amount: BigInt(splits[0]!.amount),
738
+ to: splits[0]!.recipient as Hex.Hex,
739
+ token: challenge.request.currency as Hex.Hex,
740
+ }),
741
+ ],
742
+ nonceKey: 'expiring',
743
+ } as never)
744
+ prepared.gas = prepared.gas! + 5_000n
745
+ const signature = await signTransaction(client, prepared as never)
746
+
747
+ const credential = Credential.from({
748
+ challenge,
749
+ payload: { signature, type: 'transaction' as const },
750
+ })
751
+
752
+ const authResponse = await fetch(httpServer.url, {
753
+ headers: { Authorization: Credential.serialize(credential) },
754
+ })
755
+ expect(authResponse.status).toBe(200)
756
+
757
+ httpServer.close()
758
+ })
759
+
653
760
  test('behavior: rejects expired request', async () => {
654
761
  const httpServer = await Http.createServer(async (req, res) => {
655
762
  const result = await Mppx_server.toNodeListener(
@@ -1156,6 +1263,39 @@ describe('tempo', () => {
1156
1263
  httpServer.close()
1157
1264
  })
1158
1265
 
1266
+ test('behavior: fee payer with splits', async () => {
1267
+ const mppx = Mppx_client.create({
1268
+ polyfill: false,
1269
+ methods: [
1270
+ tempo_client({
1271
+ account: accounts[1],
1272
+ getClient() {
1273
+ return client
1274
+ },
1275
+ }),
1276
+ ],
1277
+ })
1278
+
1279
+ const httpServer = await Http.createServer(async (req, res) => {
1280
+ const result = await Mppx_server.toNodeListener(
1281
+ server.charge({
1282
+ feePayer: accounts[0],
1283
+ amount: '1',
1284
+ currency: asset,
1285
+ recipient: accounts[0].address,
1286
+ splits: [{ amount: '0.2', recipient: accounts[2].address }],
1287
+ }),
1288
+ )(req, res)
1289
+ if (result.status === 402) return
1290
+ res.end('OK')
1291
+ })
1292
+
1293
+ const response = await mppx.fetch(httpServer.url)
1294
+ expect(response.status).toBe(200)
1295
+
1296
+ httpServer.close()
1297
+ })
1298
+
1159
1299
  test('behavior: fee payer (hoisted)', async () => {
1160
1300
  const mppx = Mppx_client.create({
1161
1301
  polyfill: false,
@@ -1658,6 +1798,70 @@ describe('tempo', () => {
1658
1798
 
1659
1799
  httpServer.close()
1660
1800
  })
1801
+
1802
+ test('behavior: accepts split transaction when transfers are out of order', async () => {
1803
+ const httpServer = await Http.createServer(async (req, res) => {
1804
+ const result = await Mppx_server.toNodeListener(
1805
+ server.charge({
1806
+ amount: '1',
1807
+ currency: asset,
1808
+ recipient: accounts[0].address,
1809
+ splits: [
1810
+ { amount: '0.2', recipient: accounts[2].address },
1811
+ { amount: '0.1', recipient: accounts[3].address },
1812
+ ],
1813
+ }),
1814
+ )(req, res)
1815
+ if (result.status === 402) return
1816
+ res.end('OK')
1817
+ })
1818
+
1819
+ const response = await fetch(httpServer.url)
1820
+ expect(response.status).toBe(402)
1821
+
1822
+ const challenge = Challenge.fromResponse(response, {
1823
+ methods: [tempo_client.charge()],
1824
+ })
1825
+ const splits = challenge.request.methodDetails?.splits ?? []
1826
+ const primaryAmount =
1827
+ BigInt(challenge.request.amount) - BigInt(splits[0]!.amount) - BigInt(splits[1]!.amount)
1828
+
1829
+ const prepared = await prepareTransactionRequest(client, {
1830
+ account: accounts[1]!,
1831
+ calls: [
1832
+ Actions.token.transfer.call({
1833
+ amount: BigInt(splits[0]!.amount),
1834
+ to: splits[0]!.recipient as Hex.Hex,
1835
+ token: challenge.request.currency as Hex.Hex,
1836
+ }),
1837
+ Actions.token.transfer.call({
1838
+ amount: primaryAmount,
1839
+ to: challenge.request.recipient as Hex.Hex,
1840
+ token: challenge.request.currency as Hex.Hex,
1841
+ }),
1842
+ Actions.token.transfer.call({
1843
+ amount: BigInt(splits[1]!.amount),
1844
+ to: splits[1]!.recipient as Hex.Hex,
1845
+ token: challenge.request.currency as Hex.Hex,
1846
+ }),
1847
+ ],
1848
+ nonceKey: 'expiring',
1849
+ } as never)
1850
+ prepared.gas = prepared.gas! + 5_000n
1851
+ const signature = await signTransaction(client, prepared as never)
1852
+
1853
+ const credential = Credential.from({
1854
+ challenge,
1855
+ payload: { signature, type: 'transaction' as const },
1856
+ })
1857
+
1858
+ const authResponse = await fetch(httpServer.url, {
1859
+ headers: { Authorization: Credential.serialize(credential) },
1860
+ })
1861
+ expect(authResponse.status).toBe(200)
1862
+
1863
+ httpServer.close()
1864
+ })
1661
1865
  })
1662
1866
 
1663
1867
  describe('intent: charge; type: transaction; waitForConfirmation: false', () => {
@@ -1722,6 +1926,361 @@ describe('tempo', () => {
1722
1926
  })
1723
1927
  })
1724
1928
 
1929
+ describe('intent: charge; type: proof (zero-dollar auth)', () => {
1930
+ test('default: end-to-end zero-dollar auth via SDK', async () => {
1931
+ const mppx = Mppx_client.create({
1932
+ polyfill: false,
1933
+ methods: [
1934
+ tempo_client({
1935
+ account: accounts[1],
1936
+ getClient: () => client,
1937
+ }),
1938
+ ],
1939
+ })
1940
+
1941
+ const httpServer = await Http.createServer(async (req, res) => {
1942
+ const result = await Mppx_server.toNodeListener(
1943
+ server.charge({ amount: '0', decimals: 6 }),
1944
+ )(req, res)
1945
+ if (result.status === 402) return
1946
+ res.end('OK')
1947
+ })
1948
+
1949
+ const response = await mppx.fetch(httpServer.url)
1950
+ expect(response.status).toBe(200)
1951
+
1952
+ const receipt = Receipt.fromResponse(response)
1953
+ expect(receipt.status).toBe('success')
1954
+ expect(receipt.method).toBe('tempo')
1955
+ expect(receipt.reference).toBeDefined()
1956
+
1957
+ httpServer.close()
1958
+ })
1959
+
1960
+ test('behavior: proof credential contains valid source DID', async () => {
1961
+ const httpServer = await Http.createServer(async (req, res) => {
1962
+ const result = await Mppx_server.toNodeListener(
1963
+ server.charge({ amount: '0', decimals: 6 }),
1964
+ )(req, res)
1965
+ if (result.status === 402) return
1966
+ res.end('OK')
1967
+ })
1968
+
1969
+ const response1 = await fetch(httpServer.url)
1970
+ expect(response1.status).toBe(402)
1971
+
1972
+ const challenge = Challenge.fromResponse(response1, {
1973
+ methods: [tempo_client.charge()],
1974
+ })
1975
+
1976
+ const signature = await signTypedData(client, {
1977
+ account: accounts[1],
1978
+ domain: Proof.domain(chain.id),
1979
+ types: Proof.types,
1980
+ primaryType: 'Proof',
1981
+ message: Proof.message(challenge.id),
1982
+ })
1983
+
1984
+ const credential = Credential.from({
1985
+ challenge,
1986
+ payload: { signature, type: 'proof' as const },
1987
+ source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
1988
+ })
1989
+
1990
+ const response2 = await fetch(httpServer.url, {
1991
+ headers: { Authorization: Credential.serialize(credential) },
1992
+ })
1993
+ expect(response2.status).toBe(200)
1994
+
1995
+ httpServer.close()
1996
+ })
1997
+
1998
+ test('behavior: rejects proof with wrong signer', async () => {
1999
+ const httpServer = await Http.createServer(async (req, res) => {
2000
+ const result = await Mppx_server.toNodeListener(
2001
+ server.charge({ amount: '0', decimals: 6 }),
2002
+ )(req, res)
2003
+ if (result.status === 402) return
2004
+ res.end('OK')
2005
+ })
2006
+
2007
+ const response1 = await fetch(httpServer.url)
2008
+ const challenge = Challenge.fromResponse(response1, {
2009
+ methods: [tempo_client.charge()],
2010
+ })
2011
+
2012
+ // Sign with accounts[2] but claim source is accounts[1]
2013
+ const signature = await signTypedData(client, {
2014
+ account: accounts[2],
2015
+ domain: Proof.domain(chain.id),
2016
+ types: Proof.types,
2017
+ primaryType: 'Proof',
2018
+ message: Proof.message(challenge.id),
2019
+ })
2020
+
2021
+ const credential = Credential.from({
2022
+ challenge,
2023
+ payload: { signature, type: 'proof' as const },
2024
+ source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
2025
+ })
2026
+
2027
+ const response2 = await fetch(httpServer.url, {
2028
+ headers: { Authorization: Credential.serialize(credential) },
2029
+ })
2030
+ expect(response2.status).toBe(402)
2031
+
2032
+ httpServer.close()
2033
+ })
2034
+
2035
+ test('behavior: rejects proof without source', async () => {
2036
+ const httpServer = await Http.createServer(async (req, res) => {
2037
+ const result = await Mppx_server.toNodeListener(
2038
+ server.charge({ amount: '0', decimals: 6 }),
2039
+ )(req, res)
2040
+ if (result.status === 402) return
2041
+ res.end('OK')
2042
+ })
2043
+
2044
+ const response1 = await fetch(httpServer.url)
2045
+ const challenge = Challenge.fromResponse(response1, {
2046
+ methods: [tempo_client.charge()],
2047
+ })
2048
+
2049
+ const signature = await signTypedData(client, {
2050
+ account: accounts[1],
2051
+ domain: Proof.domain(chain.id),
2052
+ types: Proof.types,
2053
+ primaryType: 'Proof',
2054
+ message: Proof.message(challenge.id),
2055
+ })
2056
+
2057
+ const credential = Credential.from({
2058
+ challenge,
2059
+ payload: { signature, type: 'proof' as const },
2060
+ // no source
2061
+ })
2062
+
2063
+ const response2 = await fetch(httpServer.url, {
2064
+ headers: { Authorization: Credential.serialize(credential) },
2065
+ })
2066
+ expect(response2.status).toBe(402)
2067
+
2068
+ httpServer.close()
2069
+ })
2070
+
2071
+ test('behavior: rejects transaction payload for zero-amount', async () => {
2072
+ const httpServer = await Http.createServer(async (req, res) => {
2073
+ const result = await Mppx_server.toNodeListener(
2074
+ server.charge({ amount: '0', decimals: 6 }),
2075
+ )(req, res)
2076
+ if (result.status === 402) return
2077
+ res.end('OK')
2078
+ })
2079
+
2080
+ const response1 = await fetch(httpServer.url)
2081
+ const challenge = Challenge.fromResponse(response1, {
2082
+ methods: [tempo_client.charge()],
2083
+ })
2084
+
2085
+ const credential = Credential.from({
2086
+ challenge,
2087
+ payload: { signature: '0xdead', type: 'transaction' as const },
2088
+ source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
2089
+ })
2090
+
2091
+ const response2 = await fetch(httpServer.url, {
2092
+ headers: { Authorization: Credential.serialize(credential) },
2093
+ })
2094
+ expect(response2.status).toBe(402)
2095
+ const body = (await response2.json()) as { detail: string }
2096
+ expect(body.detail).toContain('Zero-amount challenges require a proof credential.')
2097
+
2098
+ httpServer.close()
2099
+ })
2100
+
2101
+ test('behavior: rejects hash payload for zero-amount', async () => {
2102
+ const httpServer = await Http.createServer(async (req, res) => {
2103
+ const result = await Mppx_server.toNodeListener(
2104
+ server.charge({ amount: '0', decimals: 6 }),
2105
+ )(req, res)
2106
+ if (result.status === 402) return
2107
+ res.end('OK')
2108
+ })
2109
+
2110
+ const response1 = await fetch(httpServer.url)
2111
+ const challenge = Challenge.fromResponse(response1, {
2112
+ methods: [tempo_client.charge()],
2113
+ })
2114
+
2115
+ const credential = Credential.from({
2116
+ challenge,
2117
+ payload: {
2118
+ hash: '0x0000000000000000000000000000000000000000000000000000000000000001',
2119
+ type: 'hash' as const,
2120
+ },
2121
+ source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
2122
+ })
2123
+
2124
+ const response2 = await fetch(httpServer.url, {
2125
+ headers: { Authorization: Credential.serialize(credential) },
2126
+ })
2127
+ expect(response2.status).toBe(402)
2128
+ const body = (await response2.json()) as { detail: string }
2129
+ expect(body.detail).toContain('Zero-amount challenges require a proof credential.')
2130
+
2131
+ httpServer.close()
2132
+ })
2133
+
2134
+ test('behavior: rejects proof payload for non-zero amount', async () => {
2135
+ const httpServer = await Http.createServer(async (req, res) => {
2136
+ const result = await Mppx_server.toNodeListener(
2137
+ server.charge({ amount: '1', decimals: 6 }),
2138
+ )(req, res)
2139
+ if (result.status === 402) return
2140
+ res.end('OK')
2141
+ })
2142
+
2143
+ const response1 = await fetch(httpServer.url)
2144
+ const challenge = Challenge.fromResponse(response1, {
2145
+ methods: [tempo_client.charge()],
2146
+ })
2147
+
2148
+ const signature = await signTypedData(client, {
2149
+ account: accounts[1],
2150
+ domain: Proof.domain(chain.id),
2151
+ types: Proof.types,
2152
+ primaryType: 'Proof',
2153
+ message: Proof.message(challenge.id),
2154
+ })
2155
+
2156
+ const credential = Credential.from({
2157
+ challenge,
2158
+ payload: { signature, type: 'proof' as const },
2159
+ source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
2160
+ })
2161
+
2162
+ const response2 = await fetch(httpServer.url, {
2163
+ headers: { Authorization: Credential.serialize(credential) },
2164
+ })
2165
+ expect(response2.status).toBe(402)
2166
+ const body = (await response2.json()) as { detail: string }
2167
+ expect(body.detail).toContain('Proof credentials are only valid for zero-amount challenges.')
2168
+
2169
+ httpServer.close()
2170
+ })
2171
+
2172
+ test('behavior: receipt reference is the challenge ID', async () => {
2173
+ const httpServer = await Http.createServer(async (req, res) => {
2174
+ const result = await Mppx_server.toNodeListener(
2175
+ server.charge({ amount: '0', decimals: 6 }),
2176
+ )(req, res)
2177
+ if (result.status === 402) return
2178
+ res.end('OK')
2179
+ })
2180
+
2181
+ const response1 = await fetch(httpServer.url)
2182
+ const challenge = Challenge.fromResponse(response1, {
2183
+ methods: [tempo_client.charge()],
2184
+ })
2185
+
2186
+ const signature = await signTypedData(client, {
2187
+ account: accounts[1],
2188
+ domain: Proof.domain(chain.id),
2189
+ types: Proof.types,
2190
+ primaryType: 'Proof',
2191
+ message: Proof.message(challenge.id),
2192
+ })
2193
+
2194
+ const credential = Credential.from({
2195
+ challenge,
2196
+ payload: { signature, type: 'proof' as const },
2197
+ source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
2198
+ })
2199
+
2200
+ const response2 = await fetch(httpServer.url, {
2201
+ headers: { Authorization: Credential.serialize(credential) },
2202
+ })
2203
+ expect(response2.status).toBe(200)
2204
+ const receipt = Receipt.fromResponse(response2)
2205
+ expect(receipt.reference).toBe(challenge.id)
2206
+
2207
+ httpServer.close()
2208
+ })
2209
+
2210
+ test('behavior: rejects proof signed with wrong chainId domain', async () => {
2211
+ const httpServer = await Http.createServer(async (req, res) => {
2212
+ const result = await Mppx_server.toNodeListener(
2213
+ server.charge({ amount: '0', decimals: 6 }),
2214
+ )(req, res)
2215
+ if (result.status === 402) return
2216
+ res.end('OK')
2217
+ })
2218
+
2219
+ const response1 = await fetch(httpServer.url)
2220
+ const challenge = Challenge.fromResponse(response1, {
2221
+ methods: [tempo_client.charge()],
2222
+ })
2223
+
2224
+ // Sign with a different chainId in the EIP-712 domain
2225
+ const signature = await signTypedData(client, {
2226
+ account: accounts[1],
2227
+ domain: Proof.domain(99999),
2228
+ types: Proof.types,
2229
+ primaryType: 'Proof',
2230
+ message: Proof.message(challenge.id),
2231
+ })
2232
+
2233
+ const credential = Credential.from({
2234
+ challenge,
2235
+ payload: { signature, type: 'proof' as const },
2236
+ source: `did:pkh:eip155:${chain.id}:${accounts[1].address}`,
2237
+ })
2238
+
2239
+ const response2 = await fetch(httpServer.url, {
2240
+ headers: { Authorization: Credential.serialize(credential) },
2241
+ })
2242
+ expect(response2.status).toBe(402)
2243
+
2244
+ httpServer.close()
2245
+ })
2246
+
2247
+ test('behavior: rejects proof with malformed source DID', async () => {
2248
+ const httpServer = await Http.createServer(async (req, res) => {
2249
+ const result = await Mppx_server.toNodeListener(
2250
+ server.charge({ amount: '0', decimals: 6 }),
2251
+ )(req, res)
2252
+ if (result.status === 402) return
2253
+ res.end('OK')
2254
+ })
2255
+
2256
+ const response1 = await fetch(httpServer.url)
2257
+ const challenge = Challenge.fromResponse(response1, {
2258
+ methods: [tempo_client.charge()],
2259
+ })
2260
+
2261
+ const signature = await signTypedData(client, {
2262
+ account: accounts[1],
2263
+ domain: Proof.domain(chain.id),
2264
+ types: Proof.types,
2265
+ primaryType: 'Proof',
2266
+ message: Proof.message(challenge.id),
2267
+ })
2268
+
2269
+ const credential = Credential.from({
2270
+ challenge,
2271
+ payload: { signature, type: 'proof' as const },
2272
+ source: 'not-a-valid-did',
2273
+ })
2274
+
2275
+ const response2 = await fetch(httpServer.url, {
2276
+ headers: { Authorization: Credential.serialize(credential) },
2277
+ })
2278
+ expect(response2.status).toBe(402)
2279
+
2280
+ httpServer.close()
2281
+ })
2282
+ })
2283
+
1725
2284
  describe('intent: unknown', () => {
1726
2285
  test('behavior: returns 402 for invalid payload schema', async () => {
1727
2286
  const httpServer = await Http.createServer(async (req, res) => {
@@ -2295,6 +2854,39 @@ describe('tempo', () => {
2295
2854
  httpServer.close()
2296
2855
  })
2297
2856
 
2857
+ test('swaps via DEX when user lacks target currency for split payments', async () => {
2858
+ const mppx = Mppx_client.create({
2859
+ polyfill: false,
2860
+ methods: [
2861
+ tempo_client({
2862
+ account: swapPayer,
2863
+ autoSwap: true,
2864
+ getClient() {
2865
+ return client
2866
+ },
2867
+ }),
2868
+ ],
2869
+ })
2870
+
2871
+ const httpServer = await Http.createServer(async (req, res) => {
2872
+ const result = await Mppx_server.toNodeListener(
2873
+ server.charge({
2874
+ amount: '1',
2875
+ currency: asset,
2876
+ recipient: accounts[0]!.address,
2877
+ splits: [{ amount: '0.2', recipient: accounts[2]!.address }],
2878
+ }),
2879
+ )(req, res)
2880
+ if (result.status === 402) return
2881
+ res.end('OK')
2882
+ })
2883
+
2884
+ const response = await mppx.fetch(httpServer.url)
2885
+ expect(response.status).toBe(200)
2886
+
2887
+ httpServer.close()
2888
+ })
2889
+
2298
2890
  test('custom slippage and tokenIn', async () => {
2299
2891
  const mppx = Mppx_client.create({
2300
2892
  polyfill: false,