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
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
import type { TempoAddress as TempoAddress_types } from 'ox/tempo'
|
|
2
1
|
import { decodeFunctionData, keccak256, parseEventLogs, type TransactionReceipt } from 'viem'
|
|
3
2
|
import {
|
|
4
3
|
getTransactionReceipt,
|
|
5
4
|
sendRawTransaction,
|
|
6
5
|
sendRawTransactionSync,
|
|
7
6
|
signTransaction,
|
|
7
|
+
verifyTypedData,
|
|
8
8
|
call as viem_call,
|
|
9
9
|
} from 'viem/actions'
|
|
10
10
|
import { tempo as tempo_chain } from 'viem/chains'
|
|
11
11
|
import { Abis, Transaction } from 'viem/tempo'
|
|
12
12
|
|
|
13
|
-
import
|
|
14
|
-
import type { LooseOmit } from '../../internal/types.js'
|
|
13
|
+
import * as Expires from '../../Expires.js'
|
|
14
|
+
import type { LooseOmit, NoExtraKeys } from '../../internal/types.js'
|
|
15
15
|
import * as Method from '../../Method.js'
|
|
16
16
|
import * as Store from '../../Store.js'
|
|
17
17
|
import * as Client from '../../viem/Client.js'
|
|
18
18
|
import * as Account from '../internal/account.js'
|
|
19
19
|
import * as TempoAddress from '../internal/address.js'
|
|
20
|
+
import * as Charge_internal from '../internal/charge.js'
|
|
20
21
|
import * as defaults from '../internal/defaults.js'
|
|
21
22
|
import * as FeePayer from '../internal/fee-payer.js'
|
|
23
|
+
import * as Proof from '../internal/proof.js'
|
|
22
24
|
import * as Selectors from '../internal/selectors.js'
|
|
23
25
|
import type * as types from '../internal/types.js'
|
|
24
26
|
import * as Methods from '../Methods.js'
|
|
@@ -34,7 +36,10 @@ import * as Methods from '../Methods.js'
|
|
|
34
36
|
* ```
|
|
35
37
|
*/
|
|
36
38
|
export function charge<const parameters extends charge.Parameters>(
|
|
37
|
-
parameters: parameters = {} as
|
|
39
|
+
parameters: NoExtraKeys<parameters, charge.Parameters> = {} as NoExtraKeys<
|
|
40
|
+
parameters,
|
|
41
|
+
charge.Parameters
|
|
42
|
+
>,
|
|
38
43
|
) {
|
|
39
44
|
const {
|
|
40
45
|
amount,
|
|
@@ -115,27 +120,27 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
115
120
|
const currency = challengeRequest.currency as `0x${string}`
|
|
116
121
|
const recipient = challengeRequest.recipient as `0x${string}`
|
|
117
122
|
|
|
118
|
-
|
|
123
|
+
Expires.assert(expires, challenge.id)
|
|
119
124
|
|
|
120
125
|
const memo = methodDetails?.memo as `0x${string}` | undefined
|
|
121
126
|
|
|
122
127
|
const payload = credential.payload
|
|
128
|
+
const isZeroAmount = BigInt(amount) === 0n
|
|
129
|
+
|
|
130
|
+
if (isZeroAmount && payload.type !== 'proof')
|
|
131
|
+
throw new MismatchError('Zero-amount challenges require a proof credential.', {})
|
|
123
132
|
|
|
124
133
|
switch (payload.type) {
|
|
125
134
|
case 'hash': {
|
|
126
135
|
const hash = payload.hash as `0x${string}`
|
|
127
136
|
await assertHashUnused(store, hash)
|
|
128
137
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
assertTransferLog(receipt, {
|
|
134
|
-
amount,
|
|
138
|
+
const expectedTransfers = getExpectedTransfers({ amount, memo, methodDetails, recipient })
|
|
139
|
+
const receipt = await getTransactionReceipt(client, { hash })
|
|
140
|
+
assertTransferLogs(receipt, {
|
|
135
141
|
currency,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
recipient,
|
|
142
|
+
sender: receipt.from,
|
|
143
|
+
transfers: expectedTransfers,
|
|
139
144
|
})
|
|
140
145
|
|
|
141
146
|
await markHashUsed(store, hash)
|
|
@@ -143,6 +148,38 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
143
148
|
return toReceipt(receipt)
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
case 'proof': {
|
|
152
|
+
if (!isZeroAmount)
|
|
153
|
+
throw new MismatchError(
|
|
154
|
+
'Proof credentials are only valid for zero-amount challenges.',
|
|
155
|
+
{},
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
const expectedSource = credential.source
|
|
159
|
+
if (!expectedSource)
|
|
160
|
+
throw new MismatchError('Proof credential must include a source.', {})
|
|
161
|
+
|
|
162
|
+
const sourceAddress = expectedSource.split(':').pop() as `0x${string}`
|
|
163
|
+
const resolvedChainId = challenge.request.methodDetails?.chainId ?? chainId!
|
|
164
|
+
|
|
165
|
+
const valid = await verifyTypedData(client, {
|
|
166
|
+
address: sourceAddress,
|
|
167
|
+
domain: Proof.domain(resolvedChainId),
|
|
168
|
+
types: Proof.types,
|
|
169
|
+
primaryType: 'Proof',
|
|
170
|
+
message: Proof.message(challenge.id),
|
|
171
|
+
signature: payload.signature as `0x${string}`,
|
|
172
|
+
})
|
|
173
|
+
if (!valid) throw new MismatchError('Proof signature does not match source.', {})
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
method: 'tempo',
|
|
177
|
+
status: 'success',
|
|
178
|
+
timestamp: new Date().toISOString(),
|
|
179
|
+
reference: challenge.id,
|
|
180
|
+
} as const
|
|
181
|
+
}
|
|
182
|
+
|
|
146
183
|
case 'transaction': {
|
|
147
184
|
const serializedTransaction = payload.signature as Transaction.TransactionSerializedTempo
|
|
148
185
|
|
|
@@ -161,58 +198,15 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
161
198
|
{},
|
|
162
199
|
)
|
|
163
200
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (selector !== Selectors.transferWithMemo) return false
|
|
172
|
-
try {
|
|
173
|
-
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
174
|
-
const [to, amount_, memo_] = args as [`0x${string}`, bigint, `0x${string}`]
|
|
175
|
-
return (
|
|
176
|
-
TempoAddress.isEqual(to, recipient) &&
|
|
177
|
-
amount_.toString() === amount &&
|
|
178
|
-
memo_.toLowerCase() === memo.toLowerCase()
|
|
179
|
-
)
|
|
180
|
-
} catch {
|
|
181
|
-
return false
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (selector === Selectors.transfer) {
|
|
186
|
-
try {
|
|
187
|
-
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
188
|
-
const [to, amount_] = args as [`0x${string}`, bigint]
|
|
189
|
-
return TempoAddress.isEqual(to, recipient) && amount_.toString() === amount
|
|
190
|
-
} catch {
|
|
191
|
-
return false
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (selector === Selectors.transferWithMemo) {
|
|
196
|
-
try {
|
|
197
|
-
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
198
|
-
const [to, amount_] = args as [`0x${string}`, bigint, `0x${string}`]
|
|
199
|
-
return TempoAddress.isEqual(to, recipient) && amount_.toString() === amount
|
|
200
|
-
} catch {
|
|
201
|
-
return false
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return false
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
if (!call)
|
|
209
|
-
throw new MismatchError('Invalid transaction: no matching payment call found', {
|
|
210
|
-
amount,
|
|
211
|
-
currency,
|
|
212
|
-
recipient,
|
|
213
|
-
})
|
|
201
|
+
const calls = (transaction.calls ?? []) as readonly {
|
|
202
|
+
data?: `0x${string}` | undefined
|
|
203
|
+
to?: `0x${string}` | undefined
|
|
204
|
+
}[]
|
|
205
|
+
const transfers = getExpectedTransfers({ amount, memo, methodDetails, recipient })
|
|
206
|
+
const isFeePayerTx = !!(feePayer || feePayerUrl) && methodDetails?.feePayer !== false
|
|
207
|
+
assertTransferCalls(calls, { currency, exactCount: isFeePayerTx, transfers })
|
|
214
208
|
|
|
215
|
-
if (
|
|
209
|
+
if (isFeePayerTx)
|
|
216
210
|
FeePayer.validateCalls(transaction.calls, { amount, currency, recipient })
|
|
217
211
|
|
|
218
212
|
const resolvedFeeToken =
|
|
@@ -234,12 +228,10 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
234
228
|
const receipt = await sendRawTransactionSync(client, {
|
|
235
229
|
serializedTransaction: serializedTransaction_final,
|
|
236
230
|
})
|
|
237
|
-
|
|
238
|
-
amount,
|
|
231
|
+
assertTransferLogs(receipt, {
|
|
239
232
|
currency,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
recipient,
|
|
233
|
+
sender: transaction.from! as `0x${string}`,
|
|
234
|
+
transfers,
|
|
243
235
|
})
|
|
244
236
|
// Post-broadcast dedup: catch malleable input variants
|
|
245
237
|
// (different serialized bytes, same underlying tx) that
|
|
@@ -323,72 +315,187 @@ export declare namespace charge {
|
|
|
323
315
|
}
|
|
324
316
|
}
|
|
325
317
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
318
|
+
type ExpectedTransfer = {
|
|
319
|
+
amount: string
|
|
320
|
+
allowAnyMemo?: boolean | undefined
|
|
321
|
+
memo?: `0x${string}` | undefined
|
|
322
|
+
recipient: `0x${string}`
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getExpectedTransfers(parameters: {
|
|
326
|
+
amount: string
|
|
327
|
+
memo: `0x${string}` | undefined
|
|
328
|
+
methodDetails: { splits?: readonly Charge_internal.Split[] | undefined } | undefined
|
|
329
|
+
recipient: `0x${string}`
|
|
330
|
+
}): ExpectedTransfer[] {
|
|
331
|
+
return Charge_internal.getTransfers({
|
|
332
|
+
amount: parameters.amount,
|
|
333
|
+
methodDetails: {
|
|
334
|
+
memo: parameters.memo,
|
|
335
|
+
splits: parameters.methodDetails?.splits,
|
|
336
|
+
},
|
|
337
|
+
recipient: parameters.recipient,
|
|
338
|
+
}).map((transfer) => ({
|
|
339
|
+
...transfer,
|
|
340
|
+
...(!transfer.memo ? { allowAnyMemo: true } : {}),
|
|
341
|
+
})) as ExpectedTransfer[]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function assertTransferCalls(
|
|
345
|
+
calls: readonly { data?: `0x${string}` | undefined; to?: `0x${string}` | undefined }[],
|
|
329
346
|
parameters: {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
memo: `0x${string}` | undefined
|
|
334
|
-
recipient: TempoAddress_types.Address
|
|
347
|
+
currency: `0x${string}`
|
|
348
|
+
exactCount?: boolean | undefined
|
|
349
|
+
transfers: readonly ExpectedTransfer[]
|
|
335
350
|
},
|
|
336
|
-
)
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
if (memo) {
|
|
340
|
-
const memoLogs = parseEventLogs({
|
|
341
|
-
abi: Abis.tip20,
|
|
342
|
-
eventName: 'TransferWithMemo',
|
|
343
|
-
logs: receipt.logs,
|
|
344
|
-
})
|
|
351
|
+
) {
|
|
352
|
+
const transferCalls = getTransferCalls(calls)
|
|
345
353
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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,
|
|
354
|
+
if (parameters.exactCount && transferCalls.length !== parameters.transfers.length)
|
|
355
|
+
throw new MismatchError('Invalid transaction: no matching payment call found', {
|
|
356
|
+
expectedCalls: String(parameters.transfers.length),
|
|
357
|
+
actualCalls: String(transferCalls.length),
|
|
370
358
|
})
|
|
371
359
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
360
|
+
const used = new Set<number>()
|
|
361
|
+
|
|
362
|
+
// Match memo-specific transfers before wildcards to avoid greedy
|
|
363
|
+
// consumption of memo-bearing calls by allowAnyMemo entries.
|
|
364
|
+
const sorted = [...parameters.transfers].sort((a, b) => {
|
|
365
|
+
if (a.memo && !b.memo) return -1
|
|
366
|
+
if (!a.memo && b.memo) return 1
|
|
367
|
+
return 0
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
for (const expected of sorted) {
|
|
371
|
+
const matchIndex = transferCalls.findIndex((call, index) => {
|
|
372
|
+
if (used.has(index)) return false
|
|
373
|
+
const decoded = decodeTransferCall(call, parameters.currency)
|
|
374
|
+
if (!decoded) return false
|
|
375
|
+
|
|
376
|
+
if (!TempoAddress.isEqual(decoded.recipient, expected.recipient)) return false
|
|
377
|
+
if (decoded.amount !== expected.amount) return false
|
|
378
|
+
if (expected.memo) {
|
|
379
|
+
return decoded.memo?.toLowerCase() === expected.memo.toLowerCase()
|
|
380
|
+
}
|
|
381
|
+
if (expected.allowAnyMemo) return true
|
|
382
|
+
return decoded.memo === undefined
|
|
376
383
|
})
|
|
377
384
|
|
|
378
|
-
|
|
379
|
-
(
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
+
if (matchIndex === -1) {
|
|
386
|
+
throw new MismatchError('Invalid transaction: no matching payment call found', {
|
|
387
|
+
amount: expected.amount,
|
|
388
|
+
currency: parameters.currency,
|
|
389
|
+
recipient: expected.recipient,
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
used.add(matchIndex)
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function getTransferCalls(
|
|
398
|
+
calls: readonly { data?: `0x${string}` | undefined; to?: `0x${string}` | undefined }[],
|
|
399
|
+
) {
|
|
400
|
+
const selectors = calls.map((call) => call.data?.slice(0, 10))
|
|
401
|
+
const offset =
|
|
402
|
+
selectors[0] === Selectors.approve && selectors[1] === Selectors.swapExactAmountOut ? 2 : 0
|
|
403
|
+
const transferCalls = calls.slice(offset)
|
|
404
|
+
|
|
405
|
+
if (
|
|
406
|
+
transferCalls.length === 0 ||
|
|
407
|
+
selectors
|
|
408
|
+
.slice(offset)
|
|
409
|
+
.some(
|
|
410
|
+
(selector) => selector !== Selectors.transfer && selector !== Selectors.transferWithMemo,
|
|
411
|
+
)
|
|
412
|
+
) {
|
|
413
|
+
throw new MismatchError('Invalid transaction: no matching payment call found', {})
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return transferCalls
|
|
417
|
+
}
|
|
385
418
|
|
|
386
|
-
|
|
419
|
+
function decodeTransferCall(
|
|
420
|
+
call: { data?: `0x${string}` | undefined; to?: `0x${string}` | undefined },
|
|
421
|
+
currency: `0x${string}`,
|
|
422
|
+
) {
|
|
423
|
+
if (!call.to || !TempoAddress.isEqual(call.to, currency) || !call.data) return null
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const selector = call.data.slice(0, 10)
|
|
427
|
+
if (selector === Selectors.transfer) {
|
|
428
|
+
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
429
|
+
const [recipient, amount] = args as [`0x${string}`, bigint]
|
|
430
|
+
return { amount: amount.toString(), recipient }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (selector === Selectors.transferWithMemo) {
|
|
434
|
+
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
435
|
+
const [recipient, amount, memo] = args as [`0x${string}`, bigint, `0x${string}`]
|
|
436
|
+
return { amount: amount.toString(), memo, recipient }
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
return null
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return null
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function assertTransferLogs(
|
|
446
|
+
receipt: TransactionReceipt,
|
|
447
|
+
parameters: {
|
|
448
|
+
currency: `0x${string}`
|
|
449
|
+
sender: `0x${string}`
|
|
450
|
+
transfers: readonly ExpectedTransfer[]
|
|
451
|
+
},
|
|
452
|
+
) {
|
|
453
|
+
const transferLogs = parseEventLogs({
|
|
454
|
+
abi: Abis.tip20,
|
|
455
|
+
eventName: 'Transfer',
|
|
456
|
+
logs: receipt.logs,
|
|
457
|
+
}).map((log) => ({ ...log, kind: 'transfer' as const }))
|
|
458
|
+
|
|
459
|
+
const memoLogs = parseEventLogs({
|
|
460
|
+
abi: Abis.tip20,
|
|
461
|
+
eventName: 'TransferWithMemo',
|
|
462
|
+
logs: receipt.logs,
|
|
463
|
+
}).map((log) => ({ ...log, kind: 'memo' as const }))
|
|
464
|
+
|
|
465
|
+
const logs = [...transferLogs, ...memoLogs]
|
|
466
|
+
const used = new Set<number>()
|
|
467
|
+
|
|
468
|
+
// Match memo-specific transfers before wildcards to avoid greedy
|
|
469
|
+
// consumption of memo-bearing logs by allowAnyMemo entries.
|
|
470
|
+
const sorted = [...parameters.transfers].sort((a, b) => {
|
|
471
|
+
if (a.memo && !b.memo) return -1
|
|
472
|
+
if (!a.memo && b.memo) return 1
|
|
473
|
+
return 0
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
for (const transfer of sorted) {
|
|
477
|
+
const matchIndex = logs.findIndex((log, index) => {
|
|
478
|
+
if (used.has(index)) return false
|
|
479
|
+
if (!TempoAddress.isEqual(log.address, parameters.currency)) return false
|
|
480
|
+
if (!TempoAddress.isEqual(log.args.from, parameters.sender)) return false
|
|
481
|
+
if (!TempoAddress.isEqual(log.args.to, transfer.recipient)) return false
|
|
482
|
+
if (log.args.amount.toString() !== transfer.amount) return false
|
|
483
|
+
if (transfer.memo) {
|
|
484
|
+
return log.kind === 'memo' && log.args.memo.toLowerCase() === transfer.memo.toLowerCase()
|
|
485
|
+
}
|
|
486
|
+
if (transfer.allowAnyMemo) return log.kind === 'transfer' || log.kind === 'memo'
|
|
487
|
+
return log.kind === 'transfer'
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
if (matchIndex === -1) {
|
|
387
491
|
throw new MismatchError('Payment verification failed: no matching transfer found.', {
|
|
388
|
-
amount,
|
|
389
|
-
currency,
|
|
390
|
-
recipient,
|
|
492
|
+
amount: transfer.amount,
|
|
493
|
+
currency: parameters.currency,
|
|
494
|
+
recipient: transfer.recipient,
|
|
391
495
|
})
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
used.add(matchIndex)
|
|
392
499
|
}
|
|
393
500
|
}
|
|
394
501
|
|
|
@@ -14,7 +14,10 @@ import { session as session_, settle as settle_ } from './Session.js'
|
|
|
14
14
|
* ```
|
|
15
15
|
*/
|
|
16
16
|
export function tempo<const parameters extends tempo.Parameters>(parameters?: parameters) {
|
|
17
|
-
return [
|
|
17
|
+
return [
|
|
18
|
+
tempo.charge(parameters as charge_.Parameters as never),
|
|
19
|
+
tempo.session(parameters as session_.Parameters as never),
|
|
20
|
+
] as const
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export namespace tempo {
|