mppx 0.4.6 → 0.4.8
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 +12 -0
- package/dist/Store.d.ts +5 -4
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +22 -7
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/plugins/tempo.d.ts.map +1 -1
- package/dist/cli/plugins/tempo.js +9 -22
- package/dist/cli/plugins/tempo.js.map +1 -1
- package/dist/middlewares/elysia.d.ts.map +1 -1
- package/dist/middlewares/elysia.js +5 -1
- package/dist/middlewares/elysia.js.map +1 -1
- package/dist/proxy/Proxy.d.ts.map +1 -1
- package/dist/proxy/Proxy.js +3 -1
- package/dist/proxy/Proxy.js.map +1 -1
- package/dist/proxy/internal/Route.d.ts +2 -2
- package/dist/proxy/internal/Route.d.ts.map +1 -1
- package/dist/proxy/internal/Route.js +4 -2
- package/dist/proxy/internal/Route.js.map +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +26 -8
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +12 -1
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/address.d.ts +3 -0
- package/dist/tempo/internal/address.d.ts.map +1 -0
- package/dist/tempo/internal/address.js +4 -0
- package/dist/tempo/internal/address.js.map +1 -0
- package/dist/tempo/internal/auto-swap.js +3 -3
- package/dist/tempo/internal/auto-swap.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +4 -1
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +11 -3
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +11 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +109 -50
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +1 -1
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +39 -32
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +41 -1
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +51 -10
- package/dist/tempo/session/Chain.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +2 -0
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +4 -2
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Voucher.d.ts.map +1 -1
- package/dist/tempo/session/Voucher.js +3 -2
- package/dist/tempo/session/Voucher.js.map +1 -1
- package/package.json +6 -2
- package/src/Store.test-d.ts +58 -0
- package/src/Store.ts +6 -4
- package/src/cli/cli.test.ts +124 -0
- package/src/cli/cli.ts +19 -7
- package/src/cli/plugins/tempo.ts +17 -23
- package/src/middlewares/elysia.test.ts +89 -0
- package/src/middlewares/elysia.ts +4 -1
- package/src/proxy/Proxy.test.ts +56 -0
- package/src/proxy/Proxy.ts +6 -1
- package/src/proxy/internal/Route.test.ts +57 -0
- package/src/proxy/internal/Route.ts +3 -1
- package/src/server/Mppx.test.ts +246 -0
- package/src/server/Mppx.ts +27 -8
- package/src/tempo/client/SessionManager.ts +11 -1
- package/src/tempo/internal/address.ts +6 -0
- package/src/tempo/internal/auto-swap.ts +3 -3
- package/src/tempo/internal/fee-payer.ts +18 -4
- package/src/tempo/server/Charge.test.ts +1080 -31
- package/src/tempo/server/Charge.ts +158 -63
- package/src/tempo/server/Session.test.ts +929 -111
- package/src/tempo/server/Session.ts +48 -33
- package/src/tempo/server/Sse.test.ts +1 -0
- package/src/tempo/server/internal/transport.test.ts +29 -0
- package/src/tempo/server/internal/transport.ts +41 -2
- package/src/tempo/session/Chain.test.ts +144 -0
- package/src/tempo/session/Chain.ts +58 -10
- package/src/tempo/session/ChannelStore.test.ts +10 -0
- package/src/tempo/session/ChannelStore.ts +6 -3
- package/src/tempo/session/Sse.test.ts +1 -0
- package/src/tempo/session/Voucher.ts +3 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TempoAddress as TempoAddress_types } from 'ox/tempo'
|
|
2
|
+
import { decodeFunctionData, keccak256, parseEventLogs, type TransactionReceipt } from 'viem'
|
|
2
3
|
import {
|
|
3
4
|
getTransactionReceipt,
|
|
4
5
|
sendRawTransaction,
|
|
@@ -11,8 +12,10 @@ import { Abis, Transaction } from 'viem/tempo'
|
|
|
11
12
|
import { PaymentExpiredError } from '../../Errors.js'
|
|
12
13
|
import type { LooseOmit } from '../../internal/types.js'
|
|
13
14
|
import * as Method from '../../Method.js'
|
|
15
|
+
import * as Store from '../../Store.js'
|
|
14
16
|
import * as Client from '../../viem/Client.js'
|
|
15
17
|
import * as Account from '../internal/account.js'
|
|
18
|
+
import * as TempoAddress from '../internal/address.js'
|
|
16
19
|
import * as defaults from '../internal/defaults.js'
|
|
17
20
|
import * as FeePayer from '../internal/fee-payer.js'
|
|
18
21
|
import * as Selectors from '../internal/selectors.js'
|
|
@@ -41,6 +44,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
41
44
|
memo,
|
|
42
45
|
waitForConfirmation = true,
|
|
43
46
|
} = parameters
|
|
47
|
+
const store = (parameters.store ?? Store.memory()) as Store.Store<charge.StoreItemMap>
|
|
44
48
|
|
|
45
49
|
const { recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
|
|
46
50
|
|
|
@@ -119,74 +123,45 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
119
123
|
switch (payload.type) {
|
|
120
124
|
case 'hash': {
|
|
121
125
|
const hash = payload.hash as `0x${string}`
|
|
126
|
+
await assertHashUnused(store, hash)
|
|
127
|
+
|
|
122
128
|
const receipt = await getTransactionReceipt(client, {
|
|
123
129
|
hash,
|
|
124
130
|
})
|
|
125
131
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const match = memoLogs.find(
|
|
134
|
-
(log) =>
|
|
135
|
-
isAddressEqual(log.address, currency) &&
|
|
136
|
-
isAddressEqual(log.args.to, recipient) &&
|
|
137
|
-
log.args.amount.toString() === amount &&
|
|
138
|
-
log.args.memo.toLowerCase() === memo.toLowerCase(),
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
if (!match)
|
|
142
|
-
throw new MismatchError(
|
|
143
|
-
'Payment verification failed: no matching transfer with memo found.',
|
|
144
|
-
{
|
|
145
|
-
amount,
|
|
146
|
-
currency,
|
|
147
|
-
memo,
|
|
148
|
-
recipient,
|
|
149
|
-
},
|
|
150
|
-
)
|
|
151
|
-
} else {
|
|
152
|
-
const transferLogs = parseEventLogs({
|
|
153
|
-
abi: Abis.tip20,
|
|
154
|
-
eventName: 'Transfer',
|
|
155
|
-
logs: receipt.logs,
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
const memoLogs = parseEventLogs({
|
|
159
|
-
abi: Abis.tip20,
|
|
160
|
-
eventName: 'TransferWithMemo',
|
|
161
|
-
logs: receipt.logs,
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
const match = [...transferLogs, ...memoLogs].find(
|
|
165
|
-
(log) =>
|
|
166
|
-
isAddressEqual(log.address, currency) &&
|
|
167
|
-
isAddressEqual(log.args.to, recipient) &&
|
|
168
|
-
log.args.amount.toString() === amount,
|
|
169
|
-
)
|
|
132
|
+
assertTransferLog(receipt, {
|
|
133
|
+
amount,
|
|
134
|
+
currency,
|
|
135
|
+
from: receipt.from,
|
|
136
|
+
memo,
|
|
137
|
+
recipient,
|
|
138
|
+
})
|
|
170
139
|
|
|
171
|
-
|
|
172
|
-
throw new MismatchError('Payment verification failed: no matching transfer found.', {
|
|
173
|
-
amount,
|
|
174
|
-
currency,
|
|
175
|
-
recipient,
|
|
176
|
-
})
|
|
177
|
-
}
|
|
140
|
+
await markHashUsed(store, hash)
|
|
178
141
|
|
|
179
142
|
return toReceipt(receipt)
|
|
180
143
|
}
|
|
181
144
|
|
|
182
145
|
case 'transaction': {
|
|
183
146
|
const serializedTransaction = payload.signature as Transaction.TransactionSerializedTempo
|
|
184
|
-
const transaction = Transaction.deserialize(serializedTransaction)
|
|
185
147
|
|
|
186
|
-
|
|
148
|
+
// Pre-broadcast dedup: catch exact byte-for-byte replays early.
|
|
149
|
+
const hash = keccak256(serializedTransaction)
|
|
150
|
+
await assertHashUnused(store, hash)
|
|
151
|
+
await markHashUsed(store, hash)
|
|
152
|
+
|
|
153
|
+
if (!FeePayer.isTempoTransaction(serializedTransaction))
|
|
154
|
+
throw new MismatchError('Only Tempo (0x76/0x78) transactions are supported.', {})
|
|
155
|
+
|
|
156
|
+
const transaction = Transaction.deserialize(serializedTransaction)
|
|
157
|
+
if (!transaction.signature || !transaction.from)
|
|
158
|
+
throw new MismatchError(
|
|
159
|
+
'Transaction must be signed by the sender before fee payer co-signing.',
|
|
160
|
+
{},
|
|
161
|
+
)
|
|
187
162
|
|
|
188
|
-
const call = calls.find((call) => {
|
|
189
|
-
if (!call.to || !
|
|
163
|
+
const call = transaction.calls.find((call) => {
|
|
164
|
+
if (!call.to || !TempoAddress.isEqual(call.to, currency)) return false
|
|
190
165
|
if (!call.data) return false
|
|
191
166
|
|
|
192
167
|
const selector = call.data.slice(0, 10)
|
|
@@ -197,7 +172,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
197
172
|
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
198
173
|
const [to, amount_, memo_] = args as [`0x${string}`, bigint, `0x${string}`]
|
|
199
174
|
return (
|
|
200
|
-
|
|
175
|
+
TempoAddress.isEqual(to, recipient) &&
|
|
201
176
|
amount_.toString() === amount &&
|
|
202
177
|
memo_.toLowerCase() === memo.toLowerCase()
|
|
203
178
|
)
|
|
@@ -210,7 +185,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
210
185
|
try {
|
|
211
186
|
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
212
187
|
const [to, amount_] = args as [`0x${string}`, bigint]
|
|
213
|
-
return
|
|
188
|
+
return TempoAddress.isEqual(to, recipient) && amount_.toString() === amount
|
|
214
189
|
} catch {
|
|
215
190
|
return false
|
|
216
191
|
}
|
|
@@ -220,7 +195,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
220
195
|
try {
|
|
221
196
|
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
222
197
|
const [to, amount_] = args as [`0x${string}`, bigint, `0x${string}`]
|
|
223
|
-
return
|
|
198
|
+
return TempoAddress.isEqual(to, recipient) && amount_.toString() === amount
|
|
224
199
|
} catch {
|
|
225
200
|
return false
|
|
226
201
|
}
|
|
@@ -237,7 +212,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
237
212
|
})
|
|
238
213
|
|
|
239
214
|
if ((feePayer || feePayerUrl) && methodDetails?.feePayer !== false)
|
|
240
|
-
FeePayer.validateCalls(calls, { amount, currency, recipient })
|
|
215
|
+
FeePayer.validateCalls(transaction.calls, { amount, currency, recipient })
|
|
241
216
|
|
|
242
217
|
const resolvedFeeToken =
|
|
243
218
|
transaction.feeToken ?? defaults.currency[chainId as keyof typeof defaults.currency]
|
|
@@ -258,6 +233,21 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
258
233
|
const receipt = await sendRawTransactionSync(client, {
|
|
259
234
|
serializedTransaction: serializedTransaction_final,
|
|
260
235
|
})
|
|
236
|
+
assertTransferLog(receipt, {
|
|
237
|
+
amount,
|
|
238
|
+
currency,
|
|
239
|
+
from: transaction.from,
|
|
240
|
+
memo,
|
|
241
|
+
recipient,
|
|
242
|
+
})
|
|
243
|
+
// Post-broadcast dedup: catch malleable input variants
|
|
244
|
+
// (different serialized bytes, same underlying tx) that
|
|
245
|
+
// bypass the pre-broadcast check. Skip if the broadcast
|
|
246
|
+
// hash matches the input hash (already stored above).
|
|
247
|
+
if (receipt.transactionHash.toLowerCase() !== hash.toLowerCase()) {
|
|
248
|
+
await assertHashUnused(store, receipt.transactionHash)
|
|
249
|
+
await markHashUsed(store, receipt.transactionHash)
|
|
250
|
+
}
|
|
261
251
|
return toReceipt(receipt)
|
|
262
252
|
} else {
|
|
263
253
|
// Optimistic path: simulate to catch obvious reverts, then broadcast
|
|
@@ -267,16 +257,21 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
267
257
|
...transaction,
|
|
268
258
|
account: transaction.from,
|
|
269
259
|
feeToken: resolvedFeeToken,
|
|
270
|
-
calls,
|
|
260
|
+
calls: transaction.calls,
|
|
271
261
|
} as never)
|
|
272
|
-
const
|
|
262
|
+
const reference = await sendRawTransaction(client, {
|
|
273
263
|
serializedTransaction: serializedTransaction_final,
|
|
274
264
|
})
|
|
265
|
+
// Post-broadcast dedup: same
|
|
266
|
+
if (reference.toLowerCase() !== hash.toLowerCase()) {
|
|
267
|
+
await assertHashUnused(store, reference)
|
|
268
|
+
await markHashUsed(store, reference)
|
|
269
|
+
}
|
|
275
270
|
return {
|
|
276
271
|
method: 'tempo',
|
|
277
272
|
status: 'success',
|
|
278
273
|
timestamp: new Date().toISOString(),
|
|
279
|
-
reference
|
|
274
|
+
reference,
|
|
280
275
|
} as const
|
|
281
276
|
}
|
|
282
277
|
}
|
|
@@ -289,11 +284,20 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
289
284
|
}
|
|
290
285
|
|
|
291
286
|
export declare namespace charge {
|
|
287
|
+
type StoreItemMap = { [key: `mppx:charge:${string}`]: number }
|
|
288
|
+
|
|
292
289
|
type Defaults = LooseOmit<Method.RequestDefaults<typeof Methods.charge>, 'feePayer' | 'recipient'>
|
|
293
290
|
|
|
294
291
|
type Parameters = {
|
|
295
292
|
/** Testnet mode. */
|
|
296
293
|
testnet?: boolean | undefined
|
|
294
|
+
/**
|
|
295
|
+
* Store for transaction hash replay protection.
|
|
296
|
+
*
|
|
297
|
+
* Use a shared store in multi-instance deployments so consumed hashes are
|
|
298
|
+
* visible across all server instances.
|
|
299
|
+
*/
|
|
300
|
+
store?: Store.Store | undefined
|
|
297
301
|
/**
|
|
298
302
|
* Whether to wait for the charge transaction to confirm on-chain before
|
|
299
303
|
* responding. @default true
|
|
@@ -318,6 +322,97 @@ export declare namespace charge {
|
|
|
318
322
|
}
|
|
319
323
|
}
|
|
320
324
|
|
|
325
|
+
/** @internal */
|
|
326
|
+
function assertTransferLog(
|
|
327
|
+
receipt: TransactionReceipt,
|
|
328
|
+
parameters: {
|
|
329
|
+
amount: string
|
|
330
|
+
currency: TempoAddress_types.Address
|
|
331
|
+
from: TempoAddress_types.Address
|
|
332
|
+
memo: `0x${string}` | undefined
|
|
333
|
+
recipient: TempoAddress_types.Address
|
|
334
|
+
},
|
|
335
|
+
): void {
|
|
336
|
+
const { amount, currency, from, memo, recipient } = parameters
|
|
337
|
+
|
|
338
|
+
if (memo) {
|
|
339
|
+
const memoLogs = parseEventLogs({
|
|
340
|
+
abi: Abis.tip20,
|
|
341
|
+
eventName: 'TransferWithMemo',
|
|
342
|
+
logs: receipt.logs,
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const match = memoLogs.find(
|
|
346
|
+
(log) =>
|
|
347
|
+
TempoAddress.isEqual(log.address, currency) &&
|
|
348
|
+
TempoAddress.isEqual(log.args.from, from) &&
|
|
349
|
+
TempoAddress.isEqual(log.args.to, recipient) &&
|
|
350
|
+
log.args.amount.toString() === amount &&
|
|
351
|
+
log.args.memo.toLowerCase() === memo.toLowerCase(),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
if (!match)
|
|
355
|
+
throw new MismatchError(
|
|
356
|
+
'Payment verification failed: no matching transfer with memo found.',
|
|
357
|
+
{
|
|
358
|
+
amount,
|
|
359
|
+
currency,
|
|
360
|
+
memo,
|
|
361
|
+
recipient,
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
} else {
|
|
365
|
+
const transferLogs = parseEventLogs({
|
|
366
|
+
abi: Abis.tip20,
|
|
367
|
+
eventName: 'Transfer',
|
|
368
|
+
logs: receipt.logs,
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
const memoLogs = parseEventLogs({
|
|
372
|
+
abi: Abis.tip20,
|
|
373
|
+
eventName: 'TransferWithMemo',
|
|
374
|
+
logs: receipt.logs,
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
const match = [...transferLogs, ...memoLogs].find(
|
|
378
|
+
(log) =>
|
|
379
|
+
TempoAddress.isEqual(log.address, currency) &&
|
|
380
|
+
TempoAddress.isEqual(log.args.from, from) &&
|
|
381
|
+
TempoAddress.isEqual(log.args.to, recipient) &&
|
|
382
|
+
log.args.amount.toString() === amount,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if (!match)
|
|
386
|
+
throw new MismatchError('Payment verification failed: no matching transfer found.', {
|
|
387
|
+
amount,
|
|
388
|
+
currency,
|
|
389
|
+
recipient,
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** @internal */
|
|
395
|
+
function getHashStoreKey(hash: `0x${string}`): `mppx:charge:${string}` {
|
|
396
|
+
return `mppx:charge:${hash.toLowerCase()}`
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** @internal */
|
|
400
|
+
async function assertHashUnused(
|
|
401
|
+
store: Store.Store<charge.StoreItemMap>,
|
|
402
|
+
hash: `0x${string}`,
|
|
403
|
+
): Promise<void> {
|
|
404
|
+
const seen = await store.get(getHashStoreKey(hash))
|
|
405
|
+
if (seen !== null) throw new Error('Transaction hash has already been used.')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** @internal */
|
|
409
|
+
async function markHashUsed(
|
|
410
|
+
store: Store.Store<charge.StoreItemMap>,
|
|
411
|
+
hash: `0x${string}`,
|
|
412
|
+
): Promise<void> {
|
|
413
|
+
await store.put(getHashStoreKey(hash), Date.now())
|
|
414
|
+
}
|
|
415
|
+
|
|
321
416
|
/** @internal */
|
|
322
417
|
function toReceipt(receipt: TransactionReceipt) {
|
|
323
418
|
const { status, transactionHash } = receipt
|