mppx 0.8.13 → 0.8.14
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 +16 -0
- package/dist/Errors.d.ts +10 -0
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +11 -2
- package/dist/Errors.js.map +1 -1
- package/dist/Method.d.ts +105 -3
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js +102 -2
- package/dist/Method.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +12 -2
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/client/internal/MethodChallenge.d.ts +17 -0
- package/dist/client/internal/MethodChallenge.d.ts.map +1 -0
- package/dist/client/internal/MethodChallenge.js +11 -0
- package/dist/client/internal/MethodChallenge.js.map +1 -0
- package/dist/server/Mppx.d.ts +23 -4
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +91 -82
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +1 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +1 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +7 -2
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/legacy/client/ChannelOps.js +2 -1
- package/dist/tempo/legacy/client/ChannelOps.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +38 -77
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +289 -209
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +12 -1
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Relay.d.ts +48 -0
- package/dist/tempo/server/Relay.d.ts.map +1 -0
- package/dist/tempo/server/Relay.js +177 -0
- package/dist/tempo/server/Relay.js.map +1 -0
- package/dist/tempo/server/SponsorBudget.d.ts +59 -0
- package/dist/tempo/server/SponsorBudget.d.ts.map +1 -0
- package/dist/tempo/server/SponsorBudget.js +144 -0
- package/dist/tempo/server/SponsorBudget.js.map +1 -0
- 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/session/client/ChannelOps.d.ts +7 -1
- package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
- package/dist/tempo/session/client/ChannelOps.js +9 -4
- package/dist/tempo/session/client/ChannelOps.js.map +1 -1
- package/dist/tempo/session/client/CredentialState.d.ts +5 -1
- package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
- package/dist/tempo/session/client/CredentialState.js +37 -19
- package/dist/tempo/session/client/CredentialState.js.map +1 -1
- package/dist/tempo/session/client/Runtime.d.ts +8 -0
- package/dist/tempo/session/client/Runtime.d.ts.map +1 -1
- package/dist/tempo/session/client/Runtime.js +17 -0
- package/dist/tempo/session/client/Runtime.js.map +1 -1
- package/dist/tempo/session/client/Session.d.ts +15 -5
- package/dist/tempo/session/client/Session.d.ts.map +1 -1
- package/dist/tempo/session/client/Session.js +93 -36
- package/dist/tempo/session/client/Session.js.map +1 -1
- package/dist/tempo/session/client/SessionManager.d.ts +8 -1
- package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/session/client/SessionManager.js +54 -26
- package/dist/tempo/session/client/SessionManager.js.map +1 -1
- package/dist/tempo/session/client/Transports.d.ts +7 -5
- package/dist/tempo/session/client/Transports.d.ts.map +1 -1
- package/dist/tempo/session/client/Transports.js +48 -24
- package/dist/tempo/session/client/Transports.js.map +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Chain.js +96 -8
- package/dist/tempo/session/precompile/Chain.js.map +1 -1
- package/dist/tempo/session/precompile/Channel.d.ts +2 -0
- package/dist/tempo/session/precompile/Channel.d.ts.map +1 -1
- package/dist/tempo/session/precompile/Channel.js +4 -0
- package/dist/tempo/session/precompile/Channel.js.map +1 -1
- package/dist/tempo/session/server/RequestState.js +3 -1
- package/dist/tempo/session/server/RequestState.js.map +1 -1
- package/package.json +1 -1
- package/src/Errors.test.ts +23 -0
- package/src/Errors.ts +21 -2
- package/src/Method.test.ts +102 -1
- package/src/Method.ts +241 -5
- package/src/client/Mppx.test-d.ts +3 -3
- package/src/client/internal/Fetch.ts +16 -7
- package/src/client/internal/MethodChallenge.ts +30 -0
- package/src/server/Mppx.test-d.ts +2 -0
- package/src/server/Mppx.test.ts +205 -0
- package/src/server/Mppx.ts +167 -103
- package/src/tempo/PublicExports.test-d.ts +20 -0
- package/src/tempo/internal/fee-payer.ts +7 -3
- package/src/tempo/legacy/client/ChannelOps.test.ts +7 -0
- package/src/tempo/legacy/client/ChannelOps.ts +2 -1
- package/src/tempo/server/Charge.test.ts +337 -63
- package/src/tempo/server/Charge.ts +435 -267
- package/src/tempo/server/Methods.ts +11 -0
- package/src/tempo/server/Relay.test.ts +523 -0
- package/src/tempo/server/Relay.ts +288 -0
- package/src/tempo/server/SponsorBudget.test.ts +125 -0
- package/src/tempo/server/SponsorBudget.ts +213 -0
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/session/client/ChannelOps.test.ts +17 -2
- package/src/tempo/session/client/ChannelOps.ts +12 -4
- package/src/tempo/session/client/CredentialState.ts +61 -20
- package/src/tempo/session/client/Runtime.test.ts +12 -0
- package/src/tempo/session/client/Runtime.ts +22 -0
- package/src/tempo/session/client/Session.test.ts +235 -2
- package/src/tempo/session/client/Session.ts +130 -41
- package/src/tempo/session/client/SessionManager.test.ts +102 -4
- package/src/tempo/session/client/SessionManager.ts +71 -27
- package/src/tempo/session/client/Transports.test.ts +32 -1
- package/src/tempo/session/client/Transports.ts +79 -35
- package/src/tempo/session/precompile/Chain.integration.test.ts +19 -1
- package/src/tempo/session/precompile/Chain.test.ts +197 -10
- package/src/tempo/session/precompile/Chain.ts +102 -8
- package/src/tempo/session/precompile/Channel.test.ts +9 -0
- package/src/tempo/session/precompile/Channel.ts +5 -0
- package/src/tempo/session/server/RequestState.test.ts +8 -1
- package/src/tempo/session/server/RequestState.ts +2 -2
|
@@ -47,6 +47,17 @@ function testAccount() {
|
|
|
47
47
|
return privateKeyToAccount(generatePrivateKey())
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function sponsoredFeeExposure(credential: string) {
|
|
51
|
+
const payload = Credential.deserialize<{ signature: string; type: string }>(credential).payload
|
|
52
|
+
if (payload.type !== 'transaction') throw new Error('expected transaction credential')
|
|
53
|
+
const transaction = Transaction.deserialize(
|
|
54
|
+
payload.signature as Parameters<typeof Transaction.deserialize>[0],
|
|
55
|
+
)
|
|
56
|
+
if (transaction.gas === undefined || transaction.maxFeePerGas === undefined)
|
|
57
|
+
throw new Error('expected sponsored transaction fee fields')
|
|
58
|
+
return transaction.gas * transaction.maxFeePerGas
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
// TODO: Remove once the minimum viem version is >=2.54.0, which uses client-first Tempo call builders.
|
|
51
62
|
function tokenTransferCall(parameters: viem_token.transfer.Args) {
|
|
52
63
|
const call = Actions.token.transfer.call as unknown as {
|
|
@@ -376,17 +387,10 @@ describe('tempo', () => {
|
|
|
376
387
|
expect(await dedupStore.get(`tenant:mppx:charge:${receipt.transactionHash}`)).not.toBeNull()
|
|
377
388
|
expect(await dedupStore.get(`mppx:charge:${receipt.transactionHash}`)).toBeNull()
|
|
378
389
|
|
|
379
|
-
const response2 = await fetch(httpServer.url)
|
|
380
|
-
expect(response2.status).toBe(402)
|
|
381
|
-
|
|
382
|
-
const challenge2 = Challenge.fromResponse(response2, {
|
|
383
|
-
methods: [tempo_client.charge()],
|
|
384
|
-
})
|
|
385
|
-
|
|
386
390
|
const mixedCaseHash = `0x${receipt.transactionHash.slice(2).toUpperCase()}` as Hex.Hex
|
|
387
391
|
|
|
388
392
|
const credential2 = Credential.from({
|
|
389
|
-
challenge:
|
|
393
|
+
challenge: challenge1,
|
|
390
394
|
payload: { hash: mixedCaseHash, type: 'hash' as const },
|
|
391
395
|
})
|
|
392
396
|
|
|
@@ -462,15 +466,8 @@ describe('tempo', () => {
|
|
|
462
466
|
}
|
|
463
467
|
|
|
464
468
|
// Replay with lowercase — should be rejected
|
|
465
|
-
const response2 = await fetch(httpServer.url)
|
|
466
|
-
expect(response2.status).toBe(402)
|
|
467
|
-
|
|
468
|
-
const challenge2 = Challenge.fromResponse(response2, {
|
|
469
|
-
methods: [tempo_client.charge()],
|
|
470
|
-
})
|
|
471
|
-
|
|
472
469
|
const credential2 = Credential.from({
|
|
473
|
-
challenge:
|
|
470
|
+
challenge: challenge1,
|
|
474
471
|
payload: { hash: receipt.transactionHash.toLowerCase() as Hex.Hex, type: 'hash' as const },
|
|
475
472
|
})
|
|
476
473
|
|
|
@@ -702,17 +699,10 @@ describe('tempo', () => {
|
|
|
702
699
|
expect(response.status).toBe(200)
|
|
703
700
|
}
|
|
704
701
|
|
|
705
|
-
const response2 = await fetch(httpServer.url)
|
|
706
|
-
expect(response2.status).toBe(402)
|
|
707
|
-
|
|
708
|
-
const challenge2 = Challenge.fromResponse(response2, {
|
|
709
|
-
methods: [tempo_client.charge()],
|
|
710
|
-
})
|
|
711
|
-
|
|
712
702
|
const mixedCaseHash = `0x${receipt.transactionHash.slice(2).toUpperCase()}` as Hex.Hex
|
|
713
703
|
|
|
714
704
|
const credential2 = Credential.from({
|
|
715
|
-
challenge:
|
|
705
|
+
challenge: challenge1,
|
|
716
706
|
payload: { hash: mixedCaseHash, type: 'hash' as const },
|
|
717
707
|
})
|
|
718
708
|
|
|
@@ -786,15 +776,8 @@ describe('tempo', () => {
|
|
|
786
776
|
expect(response.status).toBe(200)
|
|
787
777
|
}
|
|
788
778
|
|
|
789
|
-
const response2 = await fetch(httpServer.url)
|
|
790
|
-
expect(response2.status).toBe(402)
|
|
791
|
-
|
|
792
|
-
const challenge2 = Challenge.fromResponse(response2, {
|
|
793
|
-
methods: [tempo_client.charge()],
|
|
794
|
-
})
|
|
795
|
-
|
|
796
779
|
const credential2 = Credential.from({
|
|
797
|
-
challenge:
|
|
780
|
+
challenge: challenge1,
|
|
798
781
|
payload: { hash: receipt.transactionHash.toLowerCase() as Hex.Hex, type: 'hash' as const },
|
|
799
782
|
})
|
|
800
783
|
|
|
@@ -1131,16 +1114,10 @@ describe('tempo', () => {
|
|
|
1131
1114
|
expect(pullAuthResponse.status).toBe(200)
|
|
1132
1115
|
|
|
1133
1116
|
const pullReceipt = Receipt.fromResponse(pullAuthResponse)
|
|
1134
|
-
|
|
1135
|
-
const replayChallengeResponse = await fetch(httpServer.url)
|
|
1136
|
-
expect(replayChallengeResponse.status).toBe(402)
|
|
1137
|
-
|
|
1138
|
-
const replayChallenge = Challenge.fromResponse(replayChallengeResponse, {
|
|
1139
|
-
methods: [tempo_client.charge()],
|
|
1140
|
-
})
|
|
1117
|
+
const pullCredential = Credential.deserialize(pullCredentialSerialized)
|
|
1141
1118
|
|
|
1142
1119
|
const replayCredential = Credential.from({
|
|
1143
|
-
challenge:
|
|
1120
|
+
challenge: pullCredential.challenge,
|
|
1144
1121
|
payload: { hash: pullReceipt.reference as Hex.Hex, type: 'hash' as const },
|
|
1145
1122
|
})
|
|
1146
1123
|
|
|
@@ -1546,6 +1523,62 @@ describe('tempo', () => {
|
|
|
1546
1523
|
httpServer.close()
|
|
1547
1524
|
})
|
|
1548
1525
|
|
|
1526
|
+
test('behavior: validates fee-payer pull credential before settlement', async () => {
|
|
1527
|
+
const chargeServer = Mppx_server.create({
|
|
1528
|
+
methods: [
|
|
1529
|
+
tempo_server.charge({
|
|
1530
|
+
getClient() {
|
|
1531
|
+
return client
|
|
1532
|
+
},
|
|
1533
|
+
currency: asset,
|
|
1534
|
+
account: accounts[0],
|
|
1535
|
+
feePayer: accounts[0],
|
|
1536
|
+
store: Store.memory(),
|
|
1537
|
+
}),
|
|
1538
|
+
],
|
|
1539
|
+
realm,
|
|
1540
|
+
secretKey,
|
|
1541
|
+
})
|
|
1542
|
+
const mppx = Mppx_client.create({
|
|
1543
|
+
polyfill: false,
|
|
1544
|
+
methods: [
|
|
1545
|
+
tempo_client({
|
|
1546
|
+
account: accounts[1],
|
|
1547
|
+
getClient() {
|
|
1548
|
+
return client
|
|
1549
|
+
},
|
|
1550
|
+
}),
|
|
1551
|
+
],
|
|
1552
|
+
})
|
|
1553
|
+
|
|
1554
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
1555
|
+
const result = await Mppx_server.toNodeListener(
|
|
1556
|
+
chargeServer.charge({
|
|
1557
|
+
amount: '1',
|
|
1558
|
+
currency: asset,
|
|
1559
|
+
recipient: accounts[0].address,
|
|
1560
|
+
}),
|
|
1561
|
+
)(req, res)
|
|
1562
|
+
if (result.status === 402) return
|
|
1563
|
+
res.end('OK')
|
|
1564
|
+
})
|
|
1565
|
+
|
|
1566
|
+
const response = await fetch(httpServer.url)
|
|
1567
|
+
expect(response.status).toBe(402)
|
|
1568
|
+
|
|
1569
|
+
const credential = await mppx.createCredential(response)
|
|
1570
|
+
const validation = await chargeServer.validateCredential(credential)
|
|
1571
|
+
|
|
1572
|
+
expect(validation.details).toMatchObject({
|
|
1573
|
+
mode: 'pull',
|
|
1574
|
+
sender: accounts[1].address.toLowerCase(),
|
|
1575
|
+
})
|
|
1576
|
+
|
|
1577
|
+
const receipt = await chargeServer.verifyCredential(credential)
|
|
1578
|
+
expect(receipt.status).toBe('success')
|
|
1579
|
+
httpServer.close()
|
|
1580
|
+
})
|
|
1581
|
+
|
|
1549
1582
|
test('behavior: fee payer simulates before broadcasting in confirmation mode', async () => {
|
|
1550
1583
|
const rpcMethods: string[] = []
|
|
1551
1584
|
const interceptingClient = createClient({
|
|
@@ -1917,26 +1950,26 @@ describe('tempo', () => {
|
|
|
1917
1950
|
httpServer.close()
|
|
1918
1951
|
})
|
|
1919
1952
|
|
|
1920
|
-
test('behavior: fee payer
|
|
1921
|
-
let
|
|
1922
|
-
let
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1953
|
+
test('behavior: fee payer allows concurrent expiring-nonce transactions from one sender', async () => {
|
|
1954
|
+
let releaseBroadcast!: () => void
|
|
1955
|
+
let resolveBroadcastStarted!: () => void
|
|
1956
|
+
const broadcastStarted = new Promise<void>((resolve) => {
|
|
1957
|
+
resolveBroadcastStarted = resolve
|
|
1925
1958
|
})
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1959
|
+
const releaseBroadcastPromise = new Promise<void>((resolve) => {
|
|
1960
|
+
releaseBroadcast = resolve
|
|
1928
1961
|
})
|
|
1929
|
-
let
|
|
1962
|
+
let heldFirstBroadcast = false
|
|
1930
1963
|
|
|
1931
1964
|
const interceptingClient = createClient({
|
|
1932
1965
|
account: accounts[0],
|
|
1933
1966
|
chain: client.chain,
|
|
1934
1967
|
transport: custom({
|
|
1935
1968
|
async request(args: any) {
|
|
1936
|
-
if (args.method === '
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
await
|
|
1969
|
+
if (args.method === 'eth_sendRawTransactionSync' && !heldFirstBroadcast) {
|
|
1970
|
+
heldFirstBroadcast = true
|
|
1971
|
+
resolveBroadcastStarted()
|
|
1972
|
+
await releaseBroadcastPromise
|
|
1940
1973
|
}
|
|
1941
1974
|
return client.transport.request(args)
|
|
1942
1975
|
},
|
|
@@ -1998,30 +2031,206 @@ describe('tempo', () => {
|
|
|
1998
2031
|
])
|
|
1999
2032
|
|
|
2000
2033
|
const first = fetch(httpServer.url, { headers: { Authorization: credential1 } })
|
|
2001
|
-
await
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
),
|
|
2006
|
-
).not.toBeNull()
|
|
2034
|
+
await broadcastStarted
|
|
2035
|
+
const sponsorBudgetKey =
|
|
2036
|
+
`mppx:charge:sponsor-budget:${chain.id}:${accounts[0].address.toLowerCase()}` as const
|
|
2037
|
+
expect(await sponsoredStore.get(sponsorBudgetKey)).not.toBeNull()
|
|
2007
2038
|
expect(
|
|
2008
2039
|
await sponsoredStore.get(
|
|
2009
|
-
`mppx:charge:sponsor:${chain.id}:${accounts[
|
|
2040
|
+
`tenant:mppx:charge:sponsor-budget:${chain.id}:${accounts[0].address.toLowerCase()}`,
|
|
2010
2041
|
),
|
|
2011
2042
|
).toBeNull()
|
|
2012
2043
|
const second = fetch(httpServer.url, { headers: { Authorization: credential2 } })
|
|
2013
2044
|
|
|
2014
2045
|
try {
|
|
2015
2046
|
const secondResponse = await second
|
|
2016
|
-
expect(secondResponse.status).toBe(
|
|
2017
|
-
const body = (await secondResponse.json()) as { detail: string }
|
|
2018
|
-
expect(body.detail).toContain('Sponsored transaction from this sender is already in flight')
|
|
2047
|
+
expect(secondResponse.status).toBe(200)
|
|
2019
2048
|
} finally {
|
|
2020
|
-
|
|
2049
|
+
releaseBroadcast()
|
|
2021
2050
|
}
|
|
2022
2051
|
|
|
2023
2052
|
const firstResponse = await first
|
|
2024
2053
|
expect(firstResponse.status).toBe(200)
|
|
2054
|
+
expect(await sponsoredStore.get(sponsorBudgetKey)).toBeNull()
|
|
2055
|
+
|
|
2056
|
+
httpServer.close()
|
|
2057
|
+
})
|
|
2058
|
+
|
|
2059
|
+
test('behavior: fee payer waits for atomic sponsor budget capacity', async () => {
|
|
2060
|
+
let releaseBroadcast!: () => void
|
|
2061
|
+
let resolveBroadcastStarted!: () => void
|
|
2062
|
+
const broadcastStarted = new Promise<void>((resolve) => {
|
|
2063
|
+
resolveBroadcastStarted = resolve
|
|
2064
|
+
})
|
|
2065
|
+
const releaseBroadcastPromise = new Promise<void>((resolve) => {
|
|
2066
|
+
releaseBroadcast = resolve
|
|
2067
|
+
})
|
|
2068
|
+
let heldFirstBroadcast = false
|
|
2069
|
+
|
|
2070
|
+
const interceptingClient = createClient({
|
|
2071
|
+
account: accounts[0],
|
|
2072
|
+
chain: client.chain,
|
|
2073
|
+
transport: custom({
|
|
2074
|
+
async request(args: any) {
|
|
2075
|
+
if (args.method === 'eth_sendRawTransactionSync' && !heldFirstBroadcast) {
|
|
2076
|
+
heldFirstBroadcast = true
|
|
2077
|
+
resolveBroadcastStarted()
|
|
2078
|
+
await releaseBroadcastPromise
|
|
2079
|
+
}
|
|
2080
|
+
return client.transport.request(args)
|
|
2081
|
+
},
|
|
2082
|
+
}),
|
|
2083
|
+
})
|
|
2084
|
+
|
|
2085
|
+
const feePayerPolicy: { maxInFlightTotalFee?: bigint } = {}
|
|
2086
|
+
const sponsoredStore = Store.memory()
|
|
2087
|
+
const serverWithBudget = Mppx_server.create({
|
|
2088
|
+
methods: [
|
|
2089
|
+
tempo_server.charge({
|
|
2090
|
+
feePayerPolicy,
|
|
2091
|
+
getClient() {
|
|
2092
|
+
return interceptingClient
|
|
2093
|
+
},
|
|
2094
|
+
currency: asset,
|
|
2095
|
+
account: accounts[0],
|
|
2096
|
+
store: sponsoredStore,
|
|
2097
|
+
}),
|
|
2098
|
+
],
|
|
2099
|
+
realm,
|
|
2100
|
+
secretKey,
|
|
2101
|
+
})
|
|
2102
|
+
|
|
2103
|
+
const mppx1 = Mppx_client.create({
|
|
2104
|
+
polyfill: false,
|
|
2105
|
+
methods: [
|
|
2106
|
+
tempo_client({
|
|
2107
|
+
account: accounts[1],
|
|
2108
|
+
getClient() {
|
|
2109
|
+
return client
|
|
2110
|
+
},
|
|
2111
|
+
}),
|
|
2112
|
+
],
|
|
2113
|
+
})
|
|
2114
|
+
const mppx2 = Mppx_client.create({
|
|
2115
|
+
polyfill: false,
|
|
2116
|
+
methods: [
|
|
2117
|
+
tempo_client({
|
|
2118
|
+
account: accounts[2],
|
|
2119
|
+
getClient() {
|
|
2120
|
+
return client
|
|
2121
|
+
},
|
|
2122
|
+
}),
|
|
2123
|
+
],
|
|
2124
|
+
})
|
|
2125
|
+
|
|
2126
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2127
|
+
const result = await Mppx_server.toNodeListener(
|
|
2128
|
+
serverWithBudget.charge({
|
|
2129
|
+
feePayer: accounts[0],
|
|
2130
|
+
amount: '1',
|
|
2131
|
+
currency: asset,
|
|
2132
|
+
recipient: accounts[0].address,
|
|
2133
|
+
}),
|
|
2134
|
+
)(req, res)
|
|
2135
|
+
if (result.status === 402) return
|
|
2136
|
+
res.end('OK')
|
|
2137
|
+
})
|
|
2138
|
+
|
|
2139
|
+
const [challengeResponse1, challengeResponse2] = await Promise.all([
|
|
2140
|
+
fetch(httpServer.url),
|
|
2141
|
+
fetch(httpServer.url),
|
|
2142
|
+
])
|
|
2143
|
+
const [credential1, credential2] = await Promise.all([
|
|
2144
|
+
mppx1.createCredential(challengeResponse1),
|
|
2145
|
+
mppx2.createCredential(challengeResponse2),
|
|
2146
|
+
])
|
|
2147
|
+
feePayerPolicy.maxInFlightTotalFee = [
|
|
2148
|
+
sponsoredFeeExposure(credential1),
|
|
2149
|
+
sponsoredFeeExposure(credential2),
|
|
2150
|
+
].reduce((maximum, exposure) => (exposure > maximum ? exposure : maximum))
|
|
2151
|
+
|
|
2152
|
+
const first = fetch(httpServer.url, { headers: { Authorization: credential1 } })
|
|
2153
|
+
await broadcastStarted
|
|
2154
|
+
const second = fetch(httpServer.url, { headers: { Authorization: credential2 } })
|
|
2155
|
+
try {
|
|
2156
|
+
const secondBeforeRelease = await Promise.race([
|
|
2157
|
+
second.then(() => 'settled' as const),
|
|
2158
|
+
new Promise<'waiting'>((resolve) => setTimeout(() => resolve('waiting'), 50)),
|
|
2159
|
+
])
|
|
2160
|
+
expect(secondBeforeRelease).toBe('waiting')
|
|
2161
|
+
} finally {
|
|
2162
|
+
releaseBroadcast()
|
|
2163
|
+
}
|
|
2164
|
+
const [firstResponse, secondResponse] = await Promise.all([first, second])
|
|
2165
|
+
expect(firstResponse.status).toBe(200)
|
|
2166
|
+
expect(secondResponse.status).toBe(200)
|
|
2167
|
+
expect(
|
|
2168
|
+
await sponsoredStore.get(
|
|
2169
|
+
`mppx:charge:sponsor-budget:${chain.id}:${accounts[0].address.toLowerCase()}`,
|
|
2170
|
+
),
|
|
2171
|
+
).toBeNull()
|
|
2172
|
+
|
|
2173
|
+
httpServer.close()
|
|
2174
|
+
})
|
|
2175
|
+
|
|
2176
|
+
test('behavior: fee payer reclaims expired sponsor budget reservations', async () => {
|
|
2177
|
+
const sponsoredStore = Store.memory()
|
|
2178
|
+
const sponsorBudgetKey =
|
|
2179
|
+
`mppx:charge:sponsor-budget:${chain.id}:${accounts[0].address.toLowerCase()}` as const
|
|
2180
|
+
await sponsoredStore.put(sponsorBudgetKey, {
|
|
2181
|
+
reservations: {
|
|
2182
|
+
stale: {
|
|
2183
|
+
expiresAt: Date.now() - 1,
|
|
2184
|
+
fee: '500000000000000000',
|
|
2185
|
+
leaseUntil: Date.now() - 1,
|
|
2186
|
+
owner: 'stale-worker',
|
|
2187
|
+
phase: 'pending',
|
|
2188
|
+
transactionHash: `0x${'01'.repeat(32)}`,
|
|
2189
|
+
},
|
|
2190
|
+
},
|
|
2191
|
+
version: 1,
|
|
2192
|
+
})
|
|
2193
|
+
const serverWithStaleBudget = Mppx_server.create({
|
|
2194
|
+
methods: [
|
|
2195
|
+
tempo_server.charge({
|
|
2196
|
+
getClient() {
|
|
2197
|
+
return client
|
|
2198
|
+
},
|
|
2199
|
+
currency: asset,
|
|
2200
|
+
account: accounts[0],
|
|
2201
|
+
store: sponsoredStore,
|
|
2202
|
+
}),
|
|
2203
|
+
],
|
|
2204
|
+
realm,
|
|
2205
|
+
secretKey,
|
|
2206
|
+
})
|
|
2207
|
+
const mppx = Mppx_client.create({
|
|
2208
|
+
polyfill: false,
|
|
2209
|
+
methods: [
|
|
2210
|
+
tempo_client({
|
|
2211
|
+
account: accounts[1],
|
|
2212
|
+
getClient() {
|
|
2213
|
+
return client
|
|
2214
|
+
},
|
|
2215
|
+
}),
|
|
2216
|
+
],
|
|
2217
|
+
})
|
|
2218
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2219
|
+
const result = await Mppx_server.toNodeListener(
|
|
2220
|
+
serverWithStaleBudget.charge({
|
|
2221
|
+
feePayer: accounts[0],
|
|
2222
|
+
amount: '1',
|
|
2223
|
+
currency: asset,
|
|
2224
|
+
recipient: accounts[0].address,
|
|
2225
|
+
}),
|
|
2226
|
+
)(req, res)
|
|
2227
|
+
if (result.status === 402) return
|
|
2228
|
+
res.end('OK')
|
|
2229
|
+
})
|
|
2230
|
+
|
|
2231
|
+
const response = await mppx.fetch(httpServer.url)
|
|
2232
|
+
expect(response.status).toBe(200)
|
|
2233
|
+
expect(await sponsoredStore.get(sponsorBudgetKey)).toBeNull()
|
|
2025
2234
|
|
|
2026
2235
|
httpServer.close()
|
|
2027
2236
|
})
|
|
@@ -2782,6 +2991,71 @@ describe('tempo', () => {
|
|
|
2782
2991
|
|
|
2783
2992
|
httpServer.close()
|
|
2784
2993
|
})
|
|
2994
|
+
|
|
2995
|
+
test('retains sponsored exposure until receipt reconciliation', async () => {
|
|
2996
|
+
const sponsoredStore = Store.memory()
|
|
2997
|
+
const serverNoWait = Mppx_server.create({
|
|
2998
|
+
methods: [
|
|
2999
|
+
tempo_server.charge({
|
|
3000
|
+
getClient() {
|
|
3001
|
+
return client
|
|
3002
|
+
},
|
|
3003
|
+
currency: asset,
|
|
3004
|
+
account: accounts[0],
|
|
3005
|
+
store: sponsoredStore,
|
|
3006
|
+
waitForConfirmation: false,
|
|
3007
|
+
}),
|
|
3008
|
+
],
|
|
3009
|
+
realm,
|
|
3010
|
+
secretKey,
|
|
3011
|
+
})
|
|
3012
|
+
const mppx = Mppx_client.create({
|
|
3013
|
+
polyfill: false,
|
|
3014
|
+
methods: [
|
|
3015
|
+
tempo_client({
|
|
3016
|
+
account: accounts[1],
|
|
3017
|
+
getClient() {
|
|
3018
|
+
return client
|
|
3019
|
+
},
|
|
3020
|
+
}),
|
|
3021
|
+
],
|
|
3022
|
+
})
|
|
3023
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
3024
|
+
const result = await Mppx_server.toNodeListener(
|
|
3025
|
+
serverNoWait.charge({
|
|
3026
|
+
feePayer: accounts[0],
|
|
3027
|
+
amount: '1',
|
|
3028
|
+
currency: asset,
|
|
3029
|
+
recipient: accounts[0].address,
|
|
3030
|
+
}),
|
|
3031
|
+
)(req, res)
|
|
3032
|
+
if (result.status === 402) return
|
|
3033
|
+
res.end('OK')
|
|
3034
|
+
})
|
|
3035
|
+
|
|
3036
|
+
const challengeResponse = await fetch(httpServer.url)
|
|
3037
|
+
const credential = await mppx.createCredential(challengeResponse)
|
|
3038
|
+
const response = await fetch(httpServer.url, {
|
|
3039
|
+
headers: { Authorization: credential },
|
|
3040
|
+
})
|
|
3041
|
+
|
|
3042
|
+
expect(response.status).toBe(200)
|
|
3043
|
+
const receipt = Receipt.fromResponse(response)
|
|
3044
|
+
const sponsorBudgetKey =
|
|
3045
|
+
`mppx:charge:sponsor-budget:${chain.id}:${accounts[0].address.toLowerCase()}` as const
|
|
3046
|
+
expect(await sponsoredStore.get(sponsorBudgetKey)).toMatchObject({
|
|
3047
|
+
reservations: {
|
|
3048
|
+
[receipt.reference]: {
|
|
3049
|
+
owner: expect.any(String),
|
|
3050
|
+
phase: 'pending',
|
|
3051
|
+
transactionHash: receipt.reference,
|
|
3052
|
+
},
|
|
3053
|
+
},
|
|
3054
|
+
version: 1,
|
|
3055
|
+
})
|
|
3056
|
+
|
|
3057
|
+
httpServer.close()
|
|
3058
|
+
})
|
|
2785
3059
|
})
|
|
2786
3060
|
|
|
2787
3061
|
describe('intent: charge; type: proof (zero-dollar auth)', () => {
|