mppx 0.6.13 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +24 -1
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/Mppx.js +3 -0
- package/dist/client/Mppx.js.map +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +3 -0
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
- package/dist/mcp-sdk/client/McpClient.js +3 -0
- package/dist/mcp-sdk/client/McpClient.js.map +1 -1
- package/dist/middlewares/express.d.ts.map +1 -1
- package/dist/middlewares/express.js +22 -0
- package/dist/middlewares/express.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +26 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +11 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +71 -6
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +53 -10
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +76 -29
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/internal/request-body.d.ts +1 -1
- package/dist/tempo/server/internal/request-body.d.ts.map +1 -1
- package/dist/tempo/server/internal/request-body.js +3 -0
- package/dist/tempo/server/internal/request-body.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +7 -0
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +16 -0
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts +1 -0
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +28 -11
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts +1 -0
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js +19 -12
- package/dist/tempo/session/Sse.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/plugins/tempo.ts +28 -1
- package/src/client/Mppx.test.ts +39 -3
- package/src/client/Mppx.ts +2 -0
- package/src/client/internal/Fetch.test.ts +22 -3
- package/src/client/internal/Fetch.ts +2 -0
- package/src/mcp-sdk/client/McpClient.test.ts +39 -2
- package/src/mcp-sdk/client/McpClient.ts +3 -0
- package/src/middlewares/express.test.ts +27 -0
- package/src/middlewares/express.ts +24 -0
- package/src/stripe/server/internal/html/package.json +1 -1
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/client/SessionManager.ts +26 -1
- package/src/tempo/internal/fee-payer.test.ts +139 -0
- package/src/tempo/internal/fee-payer.ts +85 -6
- package/src/tempo/server/Charge.test.ts +119 -0
- package/src/tempo/server/Charge.ts +70 -10
- package/src/tempo/server/Session.test.ts +327 -0
- package/src/tempo/server/Session.ts +88 -39
- package/src/tempo/server/internal/html/main.ts +10 -3
- package/src/tempo/server/internal/html/package.json +1 -1
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/request-body.test.ts +26 -0
- package/src/tempo/server/internal/request-body.ts +4 -1
- package/src/tempo/server/internal/transport.test.ts +28 -2
- package/src/tempo/server/internal/transport.ts +23 -0
- package/src/tempo/session/Chain.test.ts +140 -1
- package/src/tempo/session/Chain.ts +34 -10
- package/src/tempo/session/Sse.test.ts +25 -0
- package/src/tempo/session/Sse.ts +9 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TempoAddress } from 'ox/tempo'
|
|
2
2
|
import { TxEnvelopeTempo } from 'ox/tempo'
|
|
3
|
+
import type { Hex } from 'viem'
|
|
3
4
|
import type { Account } from 'viem'
|
|
4
5
|
import { decodeFunctionData } from 'viem'
|
|
5
6
|
import { Abis, Addresses, Transaction } from 'viem/tempo'
|
|
@@ -42,6 +43,13 @@ export type Policy = {
|
|
|
42
43
|
// Tempo transaction fields.
|
|
43
44
|
type SponsoredTransaction = ReturnType<(typeof Transaction)['deserialize']>
|
|
44
45
|
|
|
46
|
+
type ExpectedTransfer = {
|
|
47
|
+
amount: string
|
|
48
|
+
allowAnyMemo?: boolean | undefined
|
|
49
|
+
memo?: Hex | undefined
|
|
50
|
+
recipient: TempoAddress.Address
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
const preservedTransactionKeys = [
|
|
46
54
|
'accessList',
|
|
47
55
|
'calls',
|
|
@@ -122,6 +130,10 @@ function getPolicy(chainId: number, overrides: Partial<Policy> | undefined): Pol
|
|
|
122
130
|
export function validateCalls(
|
|
123
131
|
calls: readonly { data?: `0x${string}` | undefined; to?: TempoAddress.Address | undefined }[],
|
|
124
132
|
details: Record<string, string>,
|
|
133
|
+
options?: {
|
|
134
|
+
currency?: TempoAddress.Address | undefined
|
|
135
|
+
expectedTransfers?: readonly ExpectedTransfer[] | undefined
|
|
136
|
+
},
|
|
125
137
|
) {
|
|
126
138
|
if (calls.length === 0)
|
|
127
139
|
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
|
|
@@ -137,15 +149,19 @@ export function validateCalls(
|
|
|
137
149
|
}
|
|
138
150
|
|
|
139
151
|
const transferSelectors = callSelectors.slice(hasSwapPrefix ? 2 : 0)
|
|
152
|
+
if (transferSelectors.length === 0)
|
|
153
|
+
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
|
|
154
|
+
|
|
155
|
+
const expectedTransfers = options?.expectedTransfers
|
|
156
|
+
const transferLimit = expectedTransfers?.length ?? 11
|
|
140
157
|
if (
|
|
141
|
-
transferSelectors.length
|
|
142
|
-
transferSelectors.length > 11 ||
|
|
158
|
+
transferSelectors.length > transferLimit ||
|
|
143
159
|
transferSelectors.some(
|
|
144
160
|
(selector) => selector !== Selectors.transfer && selector !== Selectors.transferWithMemo,
|
|
145
|
-
)
|
|
146
|
-
|
|
161
|
+
) ||
|
|
162
|
+
(expectedTransfers && transferSelectors.length !== expectedTransfers.length)
|
|
163
|
+
)
|
|
147
164
|
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
|
|
148
|
-
}
|
|
149
165
|
|
|
150
166
|
// Bind the swap approval to the token the DEX call will actually spend.
|
|
151
167
|
const buyCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.swapExactAmountOut)
|
|
@@ -161,16 +177,79 @@ export function validateCalls(
|
|
|
161
177
|
const approveCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.approve)
|
|
162
178
|
if (approveCall) {
|
|
163
179
|
const { args } = decodeFunctionData({ abi: Abis.tip20, data: approveCall.data! })
|
|
180
|
+
const [spender, amount] = args as [`0x${string}`, bigint]
|
|
164
181
|
if (!approveCall.to || (buyArgs && !TempoAddress_internal.isEqual(approveCall.to, buyArgs[0])))
|
|
165
182
|
throw new FeePayerValidationError('approve target does not match swap tokenIn', details)
|
|
166
|
-
if (!TempoAddress_internal.isEqual(
|
|
183
|
+
if (!TempoAddress_internal.isEqual(spender, Addresses.stablecoinDex))
|
|
167
184
|
throw new FeePayerValidationError('approve spender is not the DEX', details)
|
|
185
|
+
if (buyArgs && amount !== buyArgs[3])
|
|
186
|
+
throw new FeePayerValidationError('approve amount does not match swap max input', details)
|
|
168
187
|
}
|
|
169
188
|
if (
|
|
170
189
|
buyCall &&
|
|
171
190
|
(!buyCall.to || !TempoAddress_internal.isEqual(buyCall.to, Addresses.stablecoinDex))
|
|
172
191
|
)
|
|
173
192
|
throw new FeePayerValidationError('buy target is not the DEX', details)
|
|
193
|
+
|
|
194
|
+
if (!expectedTransfers) return
|
|
195
|
+
|
|
196
|
+
const currency = options?.currency ?? (details.currency as TempoAddress.Address | undefined)
|
|
197
|
+
if (!currency) throw new FeePayerValidationError('missing payment currency', details)
|
|
198
|
+
|
|
199
|
+
if (buyArgs) {
|
|
200
|
+
const [, tokenOut, amountOut] = buyArgs
|
|
201
|
+
const expectedAmountOut = expectedTransfers.reduce(
|
|
202
|
+
(sum, transfer) => sum + BigInt(transfer.amount),
|
|
203
|
+
0n,
|
|
204
|
+
)
|
|
205
|
+
if (!TempoAddress_internal.isEqual(tokenOut, currency))
|
|
206
|
+
throw new FeePayerValidationError('swap tokenOut does not match payment currency', details)
|
|
207
|
+
if (amountOut !== expectedAmountOut)
|
|
208
|
+
throw new FeePayerValidationError('swap output does not match payment amount', details)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const transferCalls = calls.slice(hasSwapPrefix ? 2 : 0)
|
|
212
|
+
const sorted = [...expectedTransfers].sort((a, b) => {
|
|
213
|
+
if (a.memo && !b.memo) return -1
|
|
214
|
+
if (!a.memo && b.memo) return 1
|
|
215
|
+
return 0
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const used = new Set<number>()
|
|
219
|
+
for (const expected of sorted) {
|
|
220
|
+
const matchIndex = transferCalls.findIndex((call, index) => {
|
|
221
|
+
if (used.has(index)) return false
|
|
222
|
+
if (!call.to || !TempoAddress_internal.isEqual(call.to, currency) || !call.data) return false
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const selector = call.data.slice(0, 10)
|
|
226
|
+
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
227
|
+
if (selector === Selectors.transfer) {
|
|
228
|
+
const [recipient, amount] = args as [`0x${string}`, bigint]
|
|
229
|
+
if (!TempoAddress_internal.isEqual(recipient, expected.recipient)) return false
|
|
230
|
+
if (amount.toString() !== expected.amount) return false
|
|
231
|
+
return expected.memo === undefined
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (selector === Selectors.transferWithMemo) {
|
|
235
|
+
const [recipient, amount, memo] = args as [`0x${string}`, bigint, Hex]
|
|
236
|
+
if (!TempoAddress_internal.isEqual(recipient, expected.recipient)) return false
|
|
237
|
+
if (amount.toString() !== expected.amount) return false
|
|
238
|
+
if (expected.memo) return memo.toLowerCase() === expected.memo.toLowerCase()
|
|
239
|
+
return expected.allowAnyMemo === true
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return false
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
if (matchIndex === -1)
|
|
249
|
+
throw new FeePayerValidationError('payment transfer does not match challenge', details)
|
|
250
|
+
|
|
251
|
+
used.add(matchIndex)
|
|
252
|
+
}
|
|
174
253
|
}
|
|
175
254
|
|
|
176
255
|
export function prepareSponsoredTransaction(parameters: {
|
|
@@ -1579,6 +1579,104 @@ describe('tempo', () => {
|
|
|
1579
1579
|
httpServer.close()
|
|
1580
1580
|
})
|
|
1581
1581
|
|
|
1582
|
+
test('behavior: fee payer rejects concurrent in-flight transactions from one sender', async () => {
|
|
1583
|
+
let releaseSimulation!: () => void
|
|
1584
|
+
let resolveSimulationStarted!: () => void
|
|
1585
|
+
const simulationStarted = new Promise<void>((resolve) => {
|
|
1586
|
+
resolveSimulationStarted = resolve
|
|
1587
|
+
})
|
|
1588
|
+
const releaseSimulationPromise = new Promise<void>((resolve) => {
|
|
1589
|
+
releaseSimulation = resolve
|
|
1590
|
+
})
|
|
1591
|
+
let heldFirstSimulation = false
|
|
1592
|
+
|
|
1593
|
+
const interceptingClient = createClient({
|
|
1594
|
+
account: accounts[0],
|
|
1595
|
+
chain: client.chain,
|
|
1596
|
+
transport: custom({
|
|
1597
|
+
async request(args: any) {
|
|
1598
|
+
if (args.method === 'eth_call' && !heldFirstSimulation) {
|
|
1599
|
+
heldFirstSimulation = true
|
|
1600
|
+
resolveSimulationStarted()
|
|
1601
|
+
await releaseSimulationPromise
|
|
1602
|
+
}
|
|
1603
|
+
return client.transport.request(args)
|
|
1604
|
+
},
|
|
1605
|
+
}),
|
|
1606
|
+
})
|
|
1607
|
+
|
|
1608
|
+
const sponsoredStore = Store.memory()
|
|
1609
|
+
const serverWithSlowSimulation = Mppx_server.create({
|
|
1610
|
+
methods: [
|
|
1611
|
+
tempo_server.charge({
|
|
1612
|
+
getClient() {
|
|
1613
|
+
return interceptingClient
|
|
1614
|
+
},
|
|
1615
|
+
currency: asset,
|
|
1616
|
+
account: accounts[0],
|
|
1617
|
+
store: sponsoredStore,
|
|
1618
|
+
}),
|
|
1619
|
+
],
|
|
1620
|
+
realm,
|
|
1621
|
+
secretKey,
|
|
1622
|
+
})
|
|
1623
|
+
|
|
1624
|
+
const mppx = Mppx_client.create({
|
|
1625
|
+
polyfill: false,
|
|
1626
|
+
methods: [
|
|
1627
|
+
tempo_client({
|
|
1628
|
+
account: accounts[1],
|
|
1629
|
+
getClient() {
|
|
1630
|
+
return client
|
|
1631
|
+
},
|
|
1632
|
+
}),
|
|
1633
|
+
],
|
|
1634
|
+
})
|
|
1635
|
+
|
|
1636
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
1637
|
+
const result = await Mppx_server.toNodeListener(
|
|
1638
|
+
serverWithSlowSimulation.charge({
|
|
1639
|
+
feePayer: accounts[0],
|
|
1640
|
+
amount: '1',
|
|
1641
|
+
currency: asset,
|
|
1642
|
+
recipient: accounts[0].address,
|
|
1643
|
+
}),
|
|
1644
|
+
)(req, res)
|
|
1645
|
+
if (result.status === 402) return
|
|
1646
|
+
res.end('OK')
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
const [challengeResponse1, challengeResponse2] = await Promise.all([
|
|
1650
|
+
fetch(httpServer.url),
|
|
1651
|
+
fetch(httpServer.url),
|
|
1652
|
+
])
|
|
1653
|
+
expect(challengeResponse1.status).toBe(402)
|
|
1654
|
+
expect(challengeResponse2.status).toBe(402)
|
|
1655
|
+
|
|
1656
|
+
const [credential1, credential2] = await Promise.all([
|
|
1657
|
+
mppx.createCredential(challengeResponse1),
|
|
1658
|
+
mppx.createCredential(challengeResponse2),
|
|
1659
|
+
])
|
|
1660
|
+
|
|
1661
|
+
const first = fetch(httpServer.url, { headers: { Authorization: credential1 } })
|
|
1662
|
+
await simulationStarted
|
|
1663
|
+
const second = fetch(httpServer.url, { headers: { Authorization: credential2 } })
|
|
1664
|
+
|
|
1665
|
+
try {
|
|
1666
|
+
const secondResponse = await second
|
|
1667
|
+
expect(secondResponse.status).toBe(402)
|
|
1668
|
+
const body = (await secondResponse.json()) as { detail: string }
|
|
1669
|
+
expect(body.detail).toContain('Sponsored transaction from this sender is already in flight')
|
|
1670
|
+
} finally {
|
|
1671
|
+
releaseSimulation()
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
const firstResponse = await first
|
|
1675
|
+
expect(firstResponse.status).toBe(200)
|
|
1676
|
+
|
|
1677
|
+
httpServer.close()
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1582
1680
|
test('behavior: fee payer with splits', async () => {
|
|
1583
1681
|
const mppx = Mppx_client.create({
|
|
1584
1682
|
polyfill: false,
|
|
@@ -3338,6 +3436,27 @@ describe('tempo', () => {
|
|
|
3338
3436
|
})
|
|
3339
3437
|
expect(method.defaults?.recipient).toBe(accounts[0].address)
|
|
3340
3438
|
})
|
|
3439
|
+
|
|
3440
|
+
test('request keeps explicit feePayer false disabled during verification', async () => {
|
|
3441
|
+
const method = tempo_server.charge({
|
|
3442
|
+
getClient: () => client,
|
|
3443
|
+
account: accounts[0],
|
|
3444
|
+
currency: asset,
|
|
3445
|
+
feePayer: accounts[0],
|
|
3446
|
+
})
|
|
3447
|
+
|
|
3448
|
+
const normalized = await method.request!({
|
|
3449
|
+
credential: { challenge: {}, payload: {} } as never,
|
|
3450
|
+
request: {
|
|
3451
|
+
amount: '1',
|
|
3452
|
+
currency: asset,
|
|
3453
|
+
decimals: 6,
|
|
3454
|
+
feePayer: false,
|
|
3455
|
+
},
|
|
3456
|
+
} as never)
|
|
3457
|
+
|
|
3458
|
+
expect(normalized.feePayer).toBe(false)
|
|
3459
|
+
})
|
|
3341
3460
|
})
|
|
3342
3461
|
|
|
3343
3462
|
describe('default currency resolution', () => {
|
|
@@ -138,9 +138,10 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
138
138
|
throw new Error(`Client not configured with chainId ${chainId}.`)
|
|
139
139
|
|
|
140
140
|
const resolvedFeePayer = (() => {
|
|
141
|
+
if (request.feePayer === false) return credential ? false : undefined
|
|
141
142
|
const account = typeof request.feePayer === 'object' ? request.feePayer : feePayer
|
|
142
|
-
const requested =
|
|
143
|
-
if (credential) return account
|
|
143
|
+
const requested = account ?? feePayer ?? feePayerUrl
|
|
144
|
+
if (credential) return account ?? (feePayerUrl ? true : undefined)
|
|
144
145
|
if (requested) return true
|
|
145
146
|
return undefined
|
|
146
147
|
})()
|
|
@@ -167,12 +168,17 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
167
168
|
const client = await getClient({ chainId })
|
|
168
169
|
|
|
169
170
|
const { amount, methodDetails } = resolvedRequest
|
|
171
|
+
const requestAllowsFeePayer =
|
|
172
|
+
request.feePayer !== false &&
|
|
173
|
+
(request.feePayer === undefined ||
|
|
174
|
+
request.feePayer === true ||
|
|
175
|
+
typeof request.feePayer === 'object')
|
|
170
176
|
const feePayerAccount =
|
|
171
|
-
|
|
172
|
-
? request.feePayer
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
methodDetails?.feePayer === true && requestAllowsFeePayer
|
|
178
|
+
? typeof request.feePayer === 'object'
|
|
179
|
+
? request.feePayer
|
|
180
|
+
: feePayer
|
|
181
|
+
: undefined
|
|
176
182
|
const expires = challenge.expires
|
|
177
183
|
const supportedModes = methodDetails?.supportedModes as
|
|
178
184
|
| readonly Methods.ChargeMode[]
|
|
@@ -292,6 +298,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
292
298
|
}
|
|
293
299
|
|
|
294
300
|
let releaseReservation = true
|
|
301
|
+
let sponsoredSenderReservation: { chainId: number; sender: `0x${string}` } | undefined
|
|
295
302
|
|
|
296
303
|
try {
|
|
297
304
|
if (!FeePayer.isTempoTransaction(serializedTransaction))
|
|
@@ -309,7 +316,10 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
309
316
|
to?: `0x${string}` | undefined
|
|
310
317
|
}[]
|
|
311
318
|
const transfers = getExpectedTransfers({ amount, memo, methodDetails, recipient })
|
|
312
|
-
const isFeePayerTx =
|
|
319
|
+
const isFeePayerTx =
|
|
320
|
+
methodDetails?.feePayer === true &&
|
|
321
|
+
requestAllowsFeePayer &&
|
|
322
|
+
!!(feePayerAccount || feePayerUrl)
|
|
313
323
|
const matchedCalls = assertTransferCalls(calls, {
|
|
314
324
|
currency,
|
|
315
325
|
exactCount: isFeePayerTx,
|
|
@@ -321,8 +331,28 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
321
331
|
realm: challenge.realm,
|
|
322
332
|
})
|
|
323
333
|
|
|
324
|
-
if (isFeePayerTx)
|
|
325
|
-
|
|
334
|
+
if (isFeePayerTx) {
|
|
335
|
+
const reservationChainId = chainId ?? client.chain!.id
|
|
336
|
+
if (
|
|
337
|
+
!(await markSponsoredSenderInFlight(store, {
|
|
338
|
+
chainId: reservationChainId,
|
|
339
|
+
sender: transaction.from as `0x${string}`,
|
|
340
|
+
}))
|
|
341
|
+
) {
|
|
342
|
+
throw new VerificationFailedError({
|
|
343
|
+
reason: 'Sponsored transaction from this sender is already in flight',
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
sponsoredSenderReservation = {
|
|
347
|
+
chainId: reservationChainId,
|
|
348
|
+
sender: transaction.from as `0x${string}`,
|
|
349
|
+
}
|
|
350
|
+
FeePayer.validateCalls(
|
|
351
|
+
transaction.calls,
|
|
352
|
+
{ amount, currency, recipient },
|
|
353
|
+
{ currency, expectedTransfers: transfers },
|
|
354
|
+
)
|
|
355
|
+
}
|
|
326
356
|
|
|
327
357
|
const expectedFeeToken = defaults.currency[chainId as keyof typeof defaults.currency]
|
|
328
358
|
const resolvedFeeToken = transaction.feeToken ?? expectedFeeToken
|
|
@@ -413,6 +443,9 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
413
443
|
} catch (error) {
|
|
414
444
|
if (releaseReservation) await releaseHashUse(store, hash)
|
|
415
445
|
throw error
|
|
446
|
+
} finally {
|
|
447
|
+
if (sponsoredSenderReservation)
|
|
448
|
+
await releaseSponsoredSenderInFlight(store, sponsoredSenderReservation)
|
|
416
449
|
}
|
|
417
450
|
}
|
|
418
451
|
|
|
@@ -698,6 +731,14 @@ function getProofStoreKey(challengeId: string): `mppx:charge:${string}` {
|
|
|
698
731
|
return `mppx:charge:proof:${challengeId}`
|
|
699
732
|
}
|
|
700
733
|
|
|
734
|
+
/** @internal */
|
|
735
|
+
function getSponsoredSenderStoreKey(parameters: {
|
|
736
|
+
chainId: number
|
|
737
|
+
sender: `0x${string}`
|
|
738
|
+
}): `mppx:charge:${string}` {
|
|
739
|
+
return `mppx:charge:sponsor:${parameters.chainId}:${parameters.sender.toLowerCase()}`
|
|
740
|
+
}
|
|
741
|
+
|
|
701
742
|
async function markHashUsed(
|
|
702
743
|
store: Store.AtomicStore<charge.StoreItemMap>,
|
|
703
744
|
hash: `0x${string}`,
|
|
@@ -716,6 +757,25 @@ async function releaseHashUse(
|
|
|
716
757
|
await store.delete(getHashStoreKey(hash))
|
|
717
758
|
}
|
|
718
759
|
|
|
760
|
+
/** @internal */
|
|
761
|
+
async function markSponsoredSenderInFlight(
|
|
762
|
+
store: Store.AtomicStore<charge.StoreItemMap>,
|
|
763
|
+
parameters: { chainId: number; sender: `0x${string}` },
|
|
764
|
+
): Promise<boolean> {
|
|
765
|
+
return store.update(getSponsoredSenderStoreKey(parameters), (current) => {
|
|
766
|
+
if (current !== null) return { op: 'noop', result: false }
|
|
767
|
+
return { op: 'set', value: Date.now(), result: true }
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** @internal */
|
|
772
|
+
async function releaseSponsoredSenderInFlight(
|
|
773
|
+
store: Store.AtomicStore<charge.StoreItemMap>,
|
|
774
|
+
parameters: { chainId: number; sender: `0x${string}` },
|
|
775
|
+
): Promise<void> {
|
|
776
|
+
await store.delete(getSponsoredSenderStoreKey(parameters))
|
|
777
|
+
}
|
|
778
|
+
|
|
719
779
|
/** @internal */
|
|
720
780
|
async function markProofUsed(
|
|
721
781
|
store: Store.AtomicStore<charge.StoreItemMap>,
|