mppx 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/CHANGELOG.md +15 -3
  2. package/README.md +13 -13
  3. package/dist/BodyDigest.d.ts.map +1 -1
  4. package/dist/BodyDigest.js.map +1 -1
  5. package/dist/Challenge.d.ts.map +1 -1
  6. package/dist/Challenge.js.map +1 -1
  7. package/dist/Credential.d.ts.map +1 -1
  8. package/dist/Credential.js.map +1 -1
  9. package/dist/Errors.js +64 -67
  10. package/dist/Errors.js.map +1 -1
  11. package/dist/PaymentRequest.d.ts.map +1 -1
  12. package/dist/PaymentRequest.js.map +1 -1
  13. package/dist/Receipt.d.ts.map +1 -1
  14. package/dist/Receipt.js.map +1 -1
  15. package/dist/Store.d.ts +14 -4
  16. package/dist/Store.d.ts.map +1 -1
  17. package/dist/Store.js +17 -0
  18. package/dist/Store.js.map +1 -1
  19. package/dist/cli/account.d.ts.map +1 -1
  20. package/dist/cli/account.js +40 -5
  21. package/dist/cli/account.js.map +1 -1
  22. package/dist/cli/cli.d.ts.map +1 -1
  23. package/dist/cli/cli.js +24 -8
  24. package/dist/cli/cli.js.map +1 -1
  25. package/dist/cli/internal.d.ts.map +1 -1
  26. package/dist/cli/internal.js.map +1 -1
  27. package/dist/cli/plugins/stripe.d.ts.map +1 -1
  28. package/dist/cli/plugins/stripe.js.map +1 -1
  29. package/dist/cli/plugins/tempo.d.ts.map +1 -1
  30. package/dist/cli/plugins/tempo.js +11 -23
  31. package/dist/cli/plugins/tempo.js.map +1 -1
  32. package/dist/cli/utils.d.ts.map +1 -1
  33. package/dist/cli/utils.js.map +1 -1
  34. package/dist/client/internal/Fetch.d.ts +2 -0
  35. package/dist/client/internal/Fetch.d.ts.map +1 -1
  36. package/dist/client/internal/Fetch.js +1 -1
  37. package/dist/client/internal/Fetch.js.map +1 -1
  38. package/dist/internal/types.d.ts.map +1 -1
  39. package/dist/mcp-sdk/client/McpClient.d.ts.map +1 -1
  40. package/dist/mcp-sdk/client/McpClient.js +1 -1
  41. package/dist/mcp-sdk/client/McpClient.js.map +1 -1
  42. package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
  43. package/dist/mcp-sdk/server/Transport.js.map +1 -1
  44. package/dist/middlewares/elysia.d.ts.map +1 -1
  45. package/dist/middlewares/elysia.js +5 -1
  46. package/dist/middlewares/elysia.js.map +1 -1
  47. package/dist/middlewares/express.d.ts.map +1 -1
  48. package/dist/middlewares/express.js +5 -2
  49. package/dist/middlewares/express.js.map +1 -1
  50. package/dist/middlewares/hono.d.ts.map +1 -1
  51. package/dist/middlewares/hono.js.map +1 -1
  52. package/dist/proxy/Proxy.d.ts.map +1 -1
  53. package/dist/proxy/Proxy.js +3 -1
  54. package/dist/proxy/Proxy.js.map +1 -1
  55. package/dist/proxy/Service.js +1 -1
  56. package/dist/proxy/Service.js.map +1 -1
  57. package/dist/proxy/internal/Route.d.ts +2 -2
  58. package/dist/proxy/internal/Route.d.ts.map +1 -1
  59. package/dist/proxy/internal/Route.js +4 -2
  60. package/dist/proxy/internal/Route.js.map +1 -1
  61. package/dist/server/Mppx.d.ts.map +1 -1
  62. package/dist/server/Mppx.js +47 -11
  63. package/dist/server/Mppx.js.map +1 -1
  64. package/dist/server/Request.d.ts.map +1 -1
  65. package/dist/server/Request.js.map +1 -1
  66. package/dist/stripe/Methods.d.ts.map +1 -1
  67. package/dist/stripe/Methods.js.map +1 -1
  68. package/dist/tempo/Methods.d.ts.map +1 -1
  69. package/dist/tempo/Methods.js.map +1 -1
  70. package/dist/tempo/client/ChannelOps.d.ts.map +1 -1
  71. package/dist/tempo/client/ChannelOps.js.map +1 -1
  72. package/dist/tempo/client/Charge.d.ts.map +1 -1
  73. package/dist/tempo/client/Charge.js.map +1 -1
  74. package/dist/tempo/client/Session.d.ts.map +1 -1
  75. package/dist/tempo/client/Session.js.map +1 -1
  76. package/dist/tempo/client/SessionManager.d.ts.map +1 -1
  77. package/dist/tempo/client/SessionManager.js +1 -1
  78. package/dist/tempo/client/SessionManager.js.map +1 -1
  79. package/dist/tempo/internal/address.d.ts +3 -0
  80. package/dist/tempo/internal/address.d.ts.map +1 -0
  81. package/dist/tempo/internal/address.js +4 -0
  82. package/dist/tempo/internal/address.js.map +1 -0
  83. package/dist/tempo/internal/auto-swap.d.ts.map +1 -1
  84. package/dist/tempo/internal/auto-swap.js +4 -4
  85. package/dist/tempo/internal/auto-swap.js.map +1 -1
  86. package/dist/tempo/internal/fee-payer.d.ts +4 -1
  87. package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
  88. package/dist/tempo/internal/fee-payer.js +12 -4
  89. package/dist/tempo/internal/fee-payer.js.map +1 -1
  90. package/dist/tempo/server/Charge.d.ts +11 -0
  91. package/dist/tempo/server/Charge.d.ts.map +1 -1
  92. package/dist/tempo/server/Charge.js +110 -51
  93. package/dist/tempo/server/Charge.js.map +1 -1
  94. package/dist/tempo/server/Session.d.ts +1 -1
  95. package/dist/tempo/server/Session.d.ts.map +1 -1
  96. package/dist/tempo/server/Session.js +31 -23
  97. package/dist/tempo/server/Session.js.map +1 -1
  98. package/dist/tempo/server/internal/transport.d.ts +1 -1
  99. package/dist/tempo/server/internal/transport.d.ts.map +1 -1
  100. package/dist/tempo/server/internal/transport.js +41 -1
  101. package/dist/tempo/server/internal/transport.js.map +1 -1
  102. package/dist/tempo/session/Chain.d.ts.map +1 -1
  103. package/dist/tempo/session/Chain.js +51 -10
  104. package/dist/tempo/session/Chain.js.map +1 -1
  105. package/dist/tempo/session/ChannelStore.d.ts +2 -0
  106. package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
  107. package/dist/tempo/session/ChannelStore.js +4 -2
  108. package/dist/tempo/session/ChannelStore.js.map +1 -1
  109. package/dist/tempo/session/Receipt.d.ts.map +1 -1
  110. package/dist/tempo/session/Receipt.js.map +1 -1
  111. package/dist/tempo/session/Sse.d.ts.map +1 -1
  112. package/dist/tempo/session/Sse.js.map +1 -1
  113. package/dist/tempo/session/Voucher.d.ts.map +1 -1
  114. package/dist/tempo/session/Voucher.js +3 -2
  115. package/dist/tempo/session/Voucher.js.map +1 -1
  116. package/dist/viem/Client.d.ts.map +1 -1
  117. package/dist/viem/Client.js.map +1 -1
  118. package/package.json +2 -2
  119. package/src/BodyDigest.ts +1 -0
  120. package/src/Challenge.test-d.ts +1 -0
  121. package/src/Challenge.ts +1 -0
  122. package/src/Credential.ts +1 -0
  123. package/src/Errors.test.ts +27 -39
  124. package/src/Expires.test.ts +1 -0
  125. package/src/PaymentRequest.ts +1 -0
  126. package/src/Receipt.ts +1 -0
  127. package/src/Store.test-d.ts +59 -0
  128. package/src/Store.test.ts +56 -6
  129. package/src/Store.ts +31 -4
  130. package/src/cli/account.ts +65 -30
  131. package/src/cli/cli.test.ts +127 -1
  132. package/src/cli/cli.ts +23 -8
  133. package/src/cli/config.test.ts +1 -0
  134. package/src/cli/internal.ts +1 -0
  135. package/src/cli/plugins/stripe.ts +1 -0
  136. package/src/cli/plugins/tempo.ts +21 -24
  137. package/src/cli/utils.ts +1 -0
  138. package/src/client/Mppx.test-d.ts +1 -0
  139. package/src/client/internal/Fetch.browser.test.ts +1 -0
  140. package/src/client/internal/Fetch.test-d.ts +1 -0
  141. package/src/client/internal/Fetch.test.ts +1 -0
  142. package/src/client/internal/Fetch.ts +1 -1
  143. package/src/internal/constantTimeEqual.test.ts +1 -0
  144. package/src/internal/types.ts +1 -3
  145. package/src/mcp-sdk/client/McpClient.test-d.ts +1 -0
  146. package/src/mcp-sdk/client/McpClient.test.ts +1 -0
  147. package/src/mcp-sdk/client/McpClient.ts +2 -0
  148. package/src/mcp-sdk/server/Transport.test.ts +1 -0
  149. package/src/mcp-sdk/server/Transport.ts +1 -0
  150. package/src/middlewares/elysia.test.ts +90 -0
  151. package/src/middlewares/elysia.ts +5 -1
  152. package/src/middlewares/express.test.ts +62 -2
  153. package/src/middlewares/express.ts +6 -2
  154. package/src/middlewares/hono.ts +1 -0
  155. package/src/middlewares/internal/mppx.test.ts +1 -0
  156. package/src/middlewares/nextjs.test.ts +1 -0
  157. package/src/proxy/Proxy.test.ts +57 -0
  158. package/src/proxy/Proxy.ts +8 -1
  159. package/src/proxy/Service.test.ts +1 -0
  160. package/src/proxy/Service.ts +8 -2
  161. package/src/proxy/internal/Headers.test.ts +1 -0
  162. package/src/proxy/internal/Route.test.ts +57 -0
  163. package/src/proxy/internal/Route.ts +3 -1
  164. package/src/proxy/services/openai.test.ts +1 -0
  165. package/src/server/Mppx.test.ts +438 -0
  166. package/src/server/Mppx.ts +51 -13
  167. package/src/server/Request.test.ts +1 -0
  168. package/src/server/Request.ts +1 -0
  169. package/src/server/Response.test.ts +1 -0
  170. package/src/server/Transport.test.ts +1 -0
  171. package/src/stripe/Methods.ts +1 -0
  172. package/src/stripe/client/Charge.test.ts +1 -0
  173. package/src/stripe/server/Charge.test.ts +1 -0
  174. package/src/tempo/Attribution.test.ts +1 -0
  175. package/src/tempo/Methods.ts +1 -0
  176. package/src/tempo/client/ChannelOps.test.ts +1 -0
  177. package/src/tempo/client/ChannelOps.ts +1 -0
  178. package/src/tempo/client/Charge.ts +1 -0
  179. package/src/tempo/client/Session.test.ts +1 -0
  180. package/src/tempo/client/Session.ts +1 -0
  181. package/src/tempo/client/SessionManager.test.ts +28 -0
  182. package/src/tempo/client/SessionManager.ts +2 -1
  183. package/src/tempo/internal/address.ts +6 -0
  184. package/src/tempo/internal/auto-swap.test.ts +1 -0
  185. package/src/tempo/internal/auto-swap.ts +4 -3
  186. package/src/tempo/internal/defaults.test.ts +1 -0
  187. package/src/tempo/internal/fee-payer.test.ts +1 -0
  188. package/src/tempo/internal/fee-payer.ts +19 -4
  189. package/src/tempo/server/Charge.test.ts +1081 -31
  190. package/src/tempo/server/Charge.ts +159 -63
  191. package/src/tempo/server/Session.test.ts +896 -107
  192. package/src/tempo/server/Session.ts +41 -23
  193. package/src/tempo/server/Sse.test.ts +2 -0
  194. package/src/tempo/server/internal/transport.test.ts +30 -0
  195. package/src/tempo/server/internal/transport.ts +41 -2
  196. package/src/tempo/session/Chain.test.ts +145 -0
  197. package/src/tempo/session/Chain.ts +59 -10
  198. package/src/tempo/session/Channel.test.ts +1 -0
  199. package/src/tempo/session/ChannelStore.test.ts +11 -0
  200. package/src/tempo/session/ChannelStore.ts +7 -3
  201. package/src/tempo/session/Receipt.test.ts +1 -0
  202. package/src/tempo/session/Receipt.ts +1 -0
  203. package/src/tempo/session/Sse.test.ts +2 -0
  204. package/src/tempo/session/Sse.ts +1 -0
  205. package/src/tempo/session/Voucher.test.ts +1 -0
  206. package/src/tempo/session/Voucher.ts +4 -2
  207. package/src/viem/Account.test.ts +1 -0
  208. package/src/viem/Client.test.ts +1 -0
  209. package/src/viem/Client.ts +1 -0
@@ -1,4 +1,5 @@
1
- import { decodeFunctionData, isAddressEqual, parseEventLogs, type TransactionReceipt } from 'viem'
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,
@@ -8,11 +9,14 @@ import {
8
9
  } from 'viem/actions'
9
10
  import { tempo as tempo_chain } from 'viem/chains'
10
11
  import { Abis, Transaction } from 'viem/tempo'
12
+
11
13
  import { PaymentExpiredError } from '../../Errors.js'
12
14
  import type { LooseOmit } from '../../internal/types.js'
13
15
  import * as Method from '../../Method.js'
16
+ import * as Store from '../../Store.js'
14
17
  import * as Client from '../../viem/Client.js'
15
18
  import * as Account from '../internal/account.js'
19
+ import * as TempoAddress from '../internal/address.js'
16
20
  import * as defaults from '../internal/defaults.js'
17
21
  import * as FeePayer from '../internal/fee-payer.js'
18
22
  import * as Selectors from '../internal/selectors.js'
@@ -41,6 +45,7 @@ export function charge<const parameters extends charge.Parameters>(
41
45
  memo,
42
46
  waitForConfirmation = true,
43
47
  } = parameters
48
+ const store = (parameters.store ?? Store.memory()) as Store.Store<charge.StoreItemMap>
44
49
 
45
50
  const { recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
46
51
 
@@ -119,74 +124,45 @@ export function charge<const parameters extends charge.Parameters>(
119
124
  switch (payload.type) {
120
125
  case 'hash': {
121
126
  const hash = payload.hash as `0x${string}`
127
+ await assertHashUnused(store, hash)
128
+
122
129
  const receipt = await getTransactionReceipt(client, {
123
130
  hash,
124
131
  })
125
132
 
126
- if (memo) {
127
- const memoLogs = parseEventLogs({
128
- abi: Abis.tip20,
129
- eventName: 'TransferWithMemo',
130
- logs: receipt.logs,
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
- )
133
+ assertTransferLog(receipt, {
134
+ amount,
135
+ currency,
136
+ from: receipt.from,
137
+ memo,
138
+ recipient,
139
+ })
170
140
 
171
- if (!match)
172
- throw new MismatchError('Payment verification failed: no matching transfer found.', {
173
- amount,
174
- currency,
175
- recipient,
176
- })
177
- }
141
+ await markHashUsed(store, hash)
178
142
 
179
143
  return toReceipt(receipt)
180
144
  }
181
145
 
182
146
  case 'transaction': {
183
147
  const serializedTransaction = payload.signature as Transaction.TransactionSerializedTempo
184
- const transaction = Transaction.deserialize(serializedTransaction)
185
148
 
186
- const calls = transaction.calls ?? []
149
+ // Pre-broadcast dedup: catch exact byte-for-byte replays early.
150
+ const hash = keccak256(serializedTransaction)
151
+ await assertHashUnused(store, hash)
152
+ await markHashUsed(store, hash)
153
+
154
+ if (!FeePayer.isTempoTransaction(serializedTransaction))
155
+ throw new MismatchError('Only Tempo (0x76/0x78) transactions are supported.', {})
187
156
 
188
- const call = calls.find((call) => {
189
- if (!call.to || !isAddressEqual(call.to, currency)) return false
157
+ const transaction = Transaction.deserialize(serializedTransaction)
158
+ if (!transaction.signature || !transaction.from)
159
+ throw new MismatchError(
160
+ 'Transaction must be signed by the sender before fee payer co-signing.',
161
+ {},
162
+ )
163
+
164
+ const call = transaction.calls.find((call) => {
165
+ if (!call.to || !TempoAddress.isEqual(call.to, currency)) return false
190
166
  if (!call.data) return false
191
167
 
192
168
  const selector = call.data.slice(0, 10)
@@ -197,7 +173,7 @@ export function charge<const parameters extends charge.Parameters>(
197
173
  const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
198
174
  const [to, amount_, memo_] = args as [`0x${string}`, bigint, `0x${string}`]
199
175
  return (
200
- isAddressEqual(to, recipient) &&
176
+ TempoAddress.isEqual(to, recipient) &&
201
177
  amount_.toString() === amount &&
202
178
  memo_.toLowerCase() === memo.toLowerCase()
203
179
  )
@@ -210,7 +186,7 @@ export function charge<const parameters extends charge.Parameters>(
210
186
  try {
211
187
  const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
212
188
  const [to, amount_] = args as [`0x${string}`, bigint]
213
- return isAddressEqual(to, recipient) && amount_.toString() === amount
189
+ return TempoAddress.isEqual(to, recipient) && amount_.toString() === amount
214
190
  } catch {
215
191
  return false
216
192
  }
@@ -220,7 +196,7 @@ export function charge<const parameters extends charge.Parameters>(
220
196
  try {
221
197
  const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
222
198
  const [to, amount_] = args as [`0x${string}`, bigint, `0x${string}`]
223
- return isAddressEqual(to, recipient) && amount_.toString() === amount
199
+ return TempoAddress.isEqual(to, recipient) && amount_.toString() === amount
224
200
  } catch {
225
201
  return false
226
202
  }
@@ -237,7 +213,7 @@ export function charge<const parameters extends charge.Parameters>(
237
213
  })
238
214
 
239
215
  if ((feePayer || feePayerUrl) && methodDetails?.feePayer !== false)
240
- FeePayer.validateCalls(calls, { amount, currency, recipient })
216
+ FeePayer.validateCalls(transaction.calls, { amount, currency, recipient })
241
217
 
242
218
  const resolvedFeeToken =
243
219
  transaction.feeToken ?? defaults.currency[chainId as keyof typeof defaults.currency]
@@ -258,6 +234,21 @@ export function charge<const parameters extends charge.Parameters>(
258
234
  const receipt = await sendRawTransactionSync(client, {
259
235
  serializedTransaction: serializedTransaction_final,
260
236
  })
237
+ assertTransferLog(receipt, {
238
+ amount,
239
+ currency,
240
+ from: transaction.from,
241
+ memo,
242
+ recipient,
243
+ })
244
+ // Post-broadcast dedup: catch malleable input variants
245
+ // (different serialized bytes, same underlying tx) that
246
+ // bypass the pre-broadcast check. Skip if the broadcast
247
+ // hash matches the input hash (already stored above).
248
+ if (receipt.transactionHash.toLowerCase() !== hash.toLowerCase()) {
249
+ await assertHashUnused(store, receipt.transactionHash)
250
+ await markHashUsed(store, receipt.transactionHash)
251
+ }
261
252
  return toReceipt(receipt)
262
253
  } else {
263
254
  // Optimistic path: simulate to catch obvious reverts, then broadcast
@@ -267,16 +258,21 @@ export function charge<const parameters extends charge.Parameters>(
267
258
  ...transaction,
268
259
  account: transaction.from,
269
260
  feeToken: resolvedFeeToken,
270
- calls,
261
+ calls: transaction.calls,
271
262
  } as never)
272
- const hash = await sendRawTransaction(client, {
263
+ const reference = await sendRawTransaction(client, {
273
264
  serializedTransaction: serializedTransaction_final,
274
265
  })
266
+ // Post-broadcast dedup: same
267
+ if (reference.toLowerCase() !== hash.toLowerCase()) {
268
+ await assertHashUnused(store, reference)
269
+ await markHashUsed(store, reference)
270
+ }
275
271
  return {
276
272
  method: 'tempo',
277
273
  status: 'success',
278
274
  timestamp: new Date().toISOString(),
279
- reference: hash,
275
+ reference,
280
276
  } as const
281
277
  }
282
278
  }
@@ -289,11 +285,20 @@ export function charge<const parameters extends charge.Parameters>(
289
285
  }
290
286
 
291
287
  export declare namespace charge {
288
+ type StoreItemMap = { [key: `mppx:charge:${string}`]: number }
289
+
292
290
  type Defaults = LooseOmit<Method.RequestDefaults<typeof Methods.charge>, 'feePayer' | 'recipient'>
293
291
 
294
292
  type Parameters = {
295
293
  /** Testnet mode. */
296
294
  testnet?: boolean | undefined
295
+ /**
296
+ * Store for transaction hash replay protection.
297
+ *
298
+ * Use a shared store in multi-instance deployments so consumed hashes are
299
+ * visible across all server instances.
300
+ */
301
+ store?: Store.Store | undefined
297
302
  /**
298
303
  * Whether to wait for the charge transaction to confirm on-chain before
299
304
  * responding. @default true
@@ -318,6 +323,97 @@ export declare namespace charge {
318
323
  }
319
324
  }
320
325
 
326
+ /** @internal */
327
+ function assertTransferLog(
328
+ receipt: TransactionReceipt,
329
+ parameters: {
330
+ amount: string
331
+ currency: TempoAddress_types.Address
332
+ from: TempoAddress_types.Address
333
+ memo: `0x${string}` | undefined
334
+ recipient: TempoAddress_types.Address
335
+ },
336
+ ): void {
337
+ const { amount, currency, from, memo, recipient } = parameters
338
+
339
+ if (memo) {
340
+ const memoLogs = parseEventLogs({
341
+ abi: Abis.tip20,
342
+ eventName: 'TransferWithMemo',
343
+ logs: receipt.logs,
344
+ })
345
+
346
+ const match = memoLogs.find(
347
+ (log) =>
348
+ TempoAddress.isEqual(log.address, currency) &&
349
+ TempoAddress.isEqual(log.args.from, from) &&
350
+ TempoAddress.isEqual(log.args.to, recipient) &&
351
+ log.args.amount.toString() === amount &&
352
+ log.args.memo.toLowerCase() === memo.toLowerCase(),
353
+ )
354
+
355
+ if (!match)
356
+ throw new MismatchError(
357
+ 'Payment verification failed: no matching transfer with memo found.',
358
+ {
359
+ amount,
360
+ currency,
361
+ memo,
362
+ recipient,
363
+ },
364
+ )
365
+ } else {
366
+ const transferLogs = parseEventLogs({
367
+ abi: Abis.tip20,
368
+ eventName: 'Transfer',
369
+ logs: receipt.logs,
370
+ })
371
+
372
+ const memoLogs = parseEventLogs({
373
+ abi: Abis.tip20,
374
+ eventName: 'TransferWithMemo',
375
+ logs: receipt.logs,
376
+ })
377
+
378
+ const match = [...transferLogs, ...memoLogs].find(
379
+ (log) =>
380
+ TempoAddress.isEqual(log.address, currency) &&
381
+ TempoAddress.isEqual(log.args.from, from) &&
382
+ TempoAddress.isEqual(log.args.to, recipient) &&
383
+ log.args.amount.toString() === amount,
384
+ )
385
+
386
+ if (!match)
387
+ throw new MismatchError('Payment verification failed: no matching transfer found.', {
388
+ amount,
389
+ currency,
390
+ recipient,
391
+ })
392
+ }
393
+ }
394
+
395
+ /** @internal */
396
+ function getHashStoreKey(hash: `0x${string}`): `mppx:charge:${string}` {
397
+ return `mppx:charge:${hash.toLowerCase()}`
398
+ }
399
+
400
+ /** @internal */
401
+ async function assertHashUnused(
402
+ store: Store.Store<charge.StoreItemMap>,
403
+ hash: `0x${string}`,
404
+ ): Promise<void> {
405
+ const seen = await store.get(getHashStoreKey(hash))
406
+ if (seen !== null) throw new Error('Transaction hash has already been used.')
407
+ }
408
+
409
+ /** @internal */
410
+ async function markHashUsed(
411
+ store: Store.Store<charge.StoreItemMap>,
412
+ hash: `0x${string}`,
413
+ ): Promise<void> {
414
+ await store.put(getHashStoreKey(hash), Date.now())
415
+ }
416
+
321
417
  /** @internal */
322
418
  function toReceipt(receipt: TransactionReceipt) {
323
419
  const { status, transactionHash } = receipt