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.
- package/CHANGELOG.md +21 -0
- package/dist/Expires.d.ts +7 -0
- package/dist/Expires.d.ts.map +1 -1
- package/dist/Expires.js +21 -0
- package/dist/Expires.js.map +1 -1
- package/dist/internal/env.d.ts +1 -1
- package/dist/internal/env.d.ts.map +1 -1
- package/dist/internal/env.js +2 -6
- package/dist/internal/env.js.map +1 -1
- package/dist/internal/types.d.ts +23 -0
- package/dist/internal/types.d.ts.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 +55 -7
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +3 -3
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/Methods.d.ts +18 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +28 -3
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +24 -0
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +51 -9
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +18 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/account.d.ts +5 -11
- package/dist/tempo/internal/account.d.ts.map +1 -1
- package/dist/tempo/internal/charge.d.ts +20 -0
- package/dist/tempo/internal/charge.d.ts.map +1 -0
- package/dist/tempo/internal/charge.js +23 -0
- package/dist/tempo/internal/charge.js.map +1 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +15 -3
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/internal/proof.d.ts +23 -0
- package/dist/tempo/internal/proof.d.ts.map +1 -0
- package/dist/tempo/internal/proof.js +17 -0
- package/dist/tempo/internal/proof.js.map +1 -0
- package/dist/tempo/server/Charge.d.ts +20 -2
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +180 -103
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +20 -2
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -1
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +9 -4
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +18 -3
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts +18 -2
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +18 -14
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +1 -1
- package/src/Expires.ts +25 -0
- package/src/cli/cli.test.ts +230 -1
- package/src/internal/env.test.ts +12 -12
- package/src/internal/env.ts +2 -6
- package/src/internal/types.ts +25 -0
- package/src/middlewares/elysia.test.ts +127 -4
- package/src/middlewares/express.test.ts +120 -54
- package/src/middlewares/hono.test.ts +73 -34
- package/src/middlewares/nextjs.test.ts +159 -36
- package/src/server/Mppx.test.ts +373 -0
- package/src/server/Mppx.ts +64 -10
- package/src/stripe/server/Charge.ts +3 -7
- package/src/tempo/Methods.test.ts +105 -0
- package/src/tempo/Methods.ts +54 -17
- package/src/tempo/client/Charge.ts +67 -11
- package/src/tempo/internal/account.ts +7 -14
- package/src/tempo/internal/charge.test.ts +66 -0
- package/src/tempo/internal/charge.ts +43 -0
- package/src/tempo/internal/fee-payer.test.ts +33 -14
- package/src/tempo/internal/fee-payer.ts +21 -6
- package/src/tempo/internal/proof.test.ts +36 -0
- package/src/tempo/internal/proof.ts +19 -0
- package/src/tempo/server/Charge.test.ts +593 -1
- package/src/tempo/server/Charge.ts +233 -126
- package/src/tempo/server/Methods.ts +4 -1
- package/src/tempo/server/Session.test.ts +1152 -54
- package/src/tempo/server/Session.ts +26 -17
- package/src/tempo/server/internal/transport.test.ts +32 -0
- package/src/tempo/session/Chain.test.ts +60 -5
- package/src/tempo/session/Chain.ts +30 -14
- 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 {
|
|
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,
|