mppx 0.3.11 → 0.3.12

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 (40) hide show
  1. package/dist/client/Mppx.d.ts +1 -1
  2. package/dist/client/Mppx.d.ts.map +1 -1
  3. package/dist/client/internal/Fetch.d.ts +1 -1
  4. package/dist/client/internal/Fetch.d.ts.map +1 -1
  5. package/dist/client/internal/Fetch.js +23 -4
  6. package/dist/client/internal/Fetch.js.map +1 -1
  7. package/dist/tempo/client/Charge.d.ts +10 -0
  8. package/dist/tempo/client/Charge.d.ts.map +1 -1
  9. package/dist/tempo/client/Charge.js +23 -9
  10. package/dist/tempo/client/Charge.js.map +1 -1
  11. package/dist/tempo/client/Methods.d.ts +1 -0
  12. package/dist/tempo/client/Methods.d.ts.map +1 -1
  13. package/dist/tempo/internal/auto-swap.d.ts +49 -0
  14. package/dist/tempo/internal/auto-swap.d.ts.map +1 -0
  15. package/dist/tempo/internal/auto-swap.js +89 -0
  16. package/dist/tempo/internal/auto-swap.js.map +1 -0
  17. package/dist/tempo/internal/fee-payer.d.ts +15 -0
  18. package/dist/tempo/internal/fee-payer.d.ts.map +1 -0
  19. package/dist/tempo/internal/fee-payer.js +41 -0
  20. package/dist/tempo/internal/fee-payer.js.map +1 -0
  21. package/dist/tempo/internal/selectors.d.ts +5 -0
  22. package/dist/tempo/internal/selectors.d.ts.map +1 -0
  23. package/dist/tempo/internal/selectors.js +7 -0
  24. package/dist/tempo/internal/selectors.js.map +1 -0
  25. package/dist/tempo/server/Charge.d.ts.map +1 -1
  26. package/dist/tempo/server/Charge.js +8 -6
  27. package/dist/tempo/server/Charge.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/client/Mppx.test-d.ts +28 -0
  30. package/src/client/Mppx.ts +3 -3
  31. package/src/client/internal/Fetch.test.ts +410 -0
  32. package/src/client/internal/Fetch.ts +25 -7
  33. package/src/tempo/client/Charge.ts +40 -9
  34. package/src/tempo/internal/auto-swap.test.ts +113 -0
  35. package/src/tempo/internal/auto-swap.ts +141 -0
  36. package/src/tempo/internal/fee-payer.test.ts +223 -0
  37. package/src/tempo/internal/fee-payer.ts +53 -0
  38. package/src/tempo/internal/selectors.ts +10 -0
  39. package/src/tempo/server/Charge.test.ts +374 -3
  40. package/src/tempo/server/Charge.ts +9 -18
@@ -2,10 +2,12 @@ import { Challenge, Credential, Receipt } from 'mppx'
2
2
  import { Mppx as Mppx_client, tempo as tempo_client } from 'mppx/client'
3
3
  import { Mppx as Mppx_server, tempo as tempo_server } from 'mppx/server'
4
4
  import type { Hex } from 'ox'
5
- import { Actions } from 'viem/tempo'
6
- import { describe, expect, test } from 'vitest'
5
+ import { encodeFunctionData, parseUnits } from 'viem'
6
+ import { prepareTransactionRequest, signTransaction } from 'viem/actions'
7
+ import { Abis, Actions, Addresses, Tick } from 'viem/tempo'
8
+ import { beforeAll, describe, expect, test } from 'vitest'
7
9
  import * as Http from '~test/Http.js'
8
- import { accounts, asset, chain, client } from '~test/tempo/viem.js'
10
+ import { accounts, asset, chain, client, fundAccount } from '~test/tempo/viem.js'
9
11
  import * as Attribution from '../Attribution.js'
10
12
 
11
13
  const realm = 'api.example.com'
@@ -522,6 +524,71 @@ describe('tempo', () => {
522
524
 
523
525
  httpServer.close()
524
526
  })
527
+
528
+ test('error: rejects fee-payer transaction with unauthorized calls', async () => {
529
+ const httpServer = await Http.createServer(async (req, res) => {
530
+ const result = await Mppx_server.toNodeListener(
531
+ server.charge({
532
+ feePayer: accounts[0],
533
+ amount: '1',
534
+ currency: asset,
535
+ recipient: accounts[0].address,
536
+ }),
537
+ )(req, res)
538
+ if (result.status === 402) return
539
+ res.end('OK')
540
+ })
541
+
542
+ const response = await fetch(httpServer.url)
543
+ expect(response.status).toBe(402)
544
+
545
+ const challenge = Challenge.fromResponse(response, {
546
+ methods: [tempo_client.charge()],
547
+ })
548
+ const request = challenge.request
549
+
550
+ const memo = Attribution.encode({ serverId: challenge.realm })
551
+
552
+ // Build a transaction with the valid transfer + a rogue extra call
553
+ const transferCall = Actions.token.transfer.call({
554
+ amount: BigInt(request.amount),
555
+ memo,
556
+ to: request.recipient as Hex.Hex,
557
+ token: request.currency as Hex.Hex,
558
+ })
559
+
560
+ const rogueCall = {
561
+ to: request.currency as `0x${string}`,
562
+ data: encodeFunctionData({
563
+ abi: Abis.tip20,
564
+ functionName: 'transfer',
565
+ args: [accounts[2]!.address, 1n],
566
+ }),
567
+ }
568
+
569
+ const prepared = await prepareTransactionRequest(client, {
570
+ account: accounts[1]!,
571
+ calls: [transferCall, rogueCall],
572
+ nonceKey: 'expiring',
573
+ } as never)
574
+ prepared.gas = prepared.gas! + 5_000n
575
+ const signature = await signTransaction(client, prepared as never)
576
+
577
+ const credential = Credential.from({
578
+ challenge,
579
+ payload: { signature, type: 'transaction' as const },
580
+ })
581
+
582
+ {
583
+ const response = await fetch(httpServer.url, {
584
+ headers: { Authorization: Credential.serialize(credential) },
585
+ })
586
+ // Server rejects the transaction — returns 402 (error caught by handler)
587
+ expect(response.status).toBe(402)
588
+ }
589
+
590
+ httpServer.close()
591
+ })
525
592
  })
526
593
 
527
594
  describe('intent: charge; type: transaction; waitForConfirmation: false', () => {
@@ -1056,4 +1123,308 @@ describe('tempo', () => {
1056
1123
  httpServer.close()
1057
1124
  })
1058
1125
  })
1126
+
1127
+ describe('auto-swap', () => {
1128
+ // Use accounts[3] as payer with pathUsd only (no asset).
1129
+ // Use accounts[4] as payer with zero balance.
1130
+ const swapPayer = accounts[3]!
1131
+ const brokePayer = accounts[4]!
1132
+
1133
+ beforeAll(async () => {
1134
+ // Fund swap payer with pathUsd only
1135
+ await fundAccount({ address: swapPayer.address, token: Addresses.pathUsd as Hex.Hex })
1136
+
1137
+ // Seed DEX liquidity: create pair, then place a sell order for `asset`.
1138
+ await Actions.dex.createPair(client, {
1139
+ account: accounts[0]!,
1140
+ base: asset,
1141
+ })
1142
+ await fundAccount({ address: accounts[0]!.address, token: asset })
1143
+ await Actions.token.approveSync(client, {
1144
+ account: accounts[0]!,
1145
+ token: asset,
1146
+ spender: Addresses.stablecoinDex,
1147
+ amount: parseUnits('1000', 6),
1148
+ })
1149
+ await Actions.dex.placeSync(client, {
1150
+ account: accounts[0]!,
1151
+ token: asset,
1152
+ amount: parseUnits('1000', 6),
1153
+ type: 'sell',
1154
+ tick: Tick.fromPrice('1.001'),
1155
+ })
1156
+ })
1157
+
1158
+ test('swaps via DEX when user lacks target currency', async () => {
1159
+ const mppx = Mppx_client.create({
1160
+ polyfill: false,
1161
+ methods: [
1162
+ tempo_client({
1163
+ account: swapPayer,
1164
+ autoSwap: true,
1165
+ getClient() {
1166
+ return client
1167
+ },
1168
+ }),
1169
+ ],
1170
+ })
1171
+
1172
+ const httpServer = await Http.createServer(async (req, res) => {
1173
+ const result = await Mppx_server.toNodeListener(
1174
+ server.charge({
1175
+ amount: '1',
1176
+ currency: asset,
1177
+ recipient: accounts[0]!.address,
1178
+ }),
1179
+ )(req, res)
1180
+ if (result.status === 402) return
1181
+ res.end('OK')
1182
+ })
1183
+
1184
+ const response = await mppx.fetch(httpServer.url)
1185
+ expect(response.status).toBe(200)
1186
+
1187
+ const receipt = Receipt.fromResponse(response)
1188
+ expect(receipt.status).toBe('success')
1189
+ expect(receipt.method).toBe('tempo')
1190
+
1191
+ httpServer.close()
1192
+ })
1193
+
1194
+ test('direct transfer when user has target currency', async () => {
1195
+ const mppx = Mppx_client.create({
1196
+ polyfill: false,
1197
+ methods: [
1198
+ tempo_client({
1199
+ account: accounts[1]!,
1200
+ autoSwap: true,
1201
+ getClient() {
1202
+ return client
1203
+ },
1204
+ }),
1205
+ ],
1206
+ })
1207
+
1208
+ const httpServer = await Http.createServer(async (req, res) => {
1209
+ const result = await Mppx_server.toNodeListener(
1210
+ server.charge({
1211
+ amount: '1',
1212
+ currency: asset,
1213
+ recipient: accounts[0]!.address,
1214
+ }),
1215
+ )(req, res)
1216
+ if (result.status === 402) return
1217
+ res.end('OK')
1218
+ })
1219
+
1220
+ const response = await mppx.fetch(httpServer.url)
1221
+ expect(response.status).toBe(200)
1222
+
1223
+ const receipt = Receipt.fromResponse(response)
1224
+ expect(receipt.status).toBe('success')
1225
+
1226
+ httpServer.close()
1227
+ })
1228
+
1229
+ test('custom slippage and tokenIn', async () => {
1230
+ const mppx = Mppx_client.create({
1231
+ polyfill: false,
1232
+ methods: [
1233
+ tempo_client({
1234
+ account: swapPayer,
1235
+ autoSwap: {
1236
+ slippage: 2,
1237
+ tokenIn: [Addresses.pathUsd],
1238
+ },
1239
+ getClient() {
1240
+ return client
1241
+ },
1242
+ }),
1243
+ ],
1244
+ })
1245
+
1246
+ const httpServer = await Http.createServer(async (req, res) => {
1247
+ const result = await Mppx_server.toNodeListener(
1248
+ server.charge({
1249
+ amount: '1',
1250
+ currency: asset,
1251
+ recipient: accounts[0]!.address,
1252
+ }),
1253
+ )(req, res)
1254
+ if (result.status === 402) return
1255
+ res.end('OK')
1256
+ })
1257
+
1258
+ const response = await mppx.fetch(httpServer.url)
1259
+ expect(response.status).toBe(200)
1260
+
1261
+ httpServer.close()
1262
+ })
1263
+
1264
+ test('autoSwap enabled via fetch context', async () => {
1265
+ const mppx = Mppx_client.create({
1266
+ polyfill: false,
1267
+ methods: [
1268
+ tempo_client({
1269
+ account: swapPayer,
1270
+ getClient() {
1271
+ return client
1272
+ },
1273
+ }),
1274
+ ],
1275
+ })
1276
+
1277
+ const httpServer = await Http.createServer(async (req, res) => {
1278
+ const result = await Mppx_server.toNodeListener(
1279
+ server.charge({
1280
+ amount: '1',
1281
+ currency: asset,
1282
+ recipient: accounts[0]!.address,
1283
+ }),
1284
+ )(req, res)
1285
+ if (result.status === 402) return
1286
+ res.end('OK')
1287
+ })
1288
+
1289
+ const response = await mppx.fetch(httpServer.url, {
1290
+ context: { autoSwap: true },
1291
+ })
1292
+ expect(response.status).toBe(200)
1293
+
1294
+ const receipt = Receipt.fromResponse(response)
1295
+ expect(receipt.status).toBe('success')
1296
+
1297
+ httpServer.close()
1298
+ })
1299
+
1300
+ test('autoSwap with custom options via fetch context', async () => {
1301
+ const mppx = Mppx_client.create({
1302
+ polyfill: false,
1303
+ methods: [
1304
+ tempo_client({
1305
+ account: swapPayer,
1306
+ getClient() {
1307
+ return client
1308
+ },
1309
+ }),
1310
+ ],
1311
+ })
1312
+
1313
+ const httpServer = await Http.createServer(async (req, res) => {
1314
+ const result = await Mppx_server.toNodeListener(
1315
+ server.charge({
1316
+ amount: '1',
1317
+ currency: asset,
1318
+ recipient: accounts[0]!.address,
1319
+ }),
1320
+ )(req, res)
1321
+ if (result.status === 402) return
1322
+ res.end('OK')
1323
+ })
1324
+
1325
+ const response = await mppx.fetch(httpServer.url, {
1326
+ context: {
1327
+ autoSwap: { slippage: 2, tokenIn: [Addresses.pathUsd] },
1328
+ },
1329
+ })
1330
+ expect(response.status).toBe(200)
1331
+
1332
+ httpServer.close()
1333
+ })
1334
+
1335
+ test('error: throws when no fallback currency has sufficient balance', async () => {
1336
+ const mppx = Mppx_client.create({
1337
+ polyfill: false,
1338
+ methods: [
1339
+ tempo_client({
1340
+ account: brokePayer,
1341
+ autoSwap: true,
1342
+ getClient() {
1343
+ return client
1344
+ },
1345
+ }),
1346
+ ],
1347
+ })
1348
+
1349
+ const httpServer = await Http.createServer(async (req, res) => {
1350
+ const result = await Mppx_server.toNodeListener(
1351
+ server.charge({
1352
+ amount: '1',
1353
+ currency: asset,
1354
+ recipient: accounts[0]!.address,
1355
+ }),
1356
+ )(req, res)
1357
+ if (result.status === 402) return
1358
+ res.end('OK')
1359
+ })
1360
+
1361
+ await expect(mppx.fetch(httpServer.url)).rejects.toThrow('Insufficient funds')
1362
+
1363
+ httpServer.close()
1364
+ })
1365
+
1366
+ test('error: throws when amount exceeds swap liquidity', async () => {
1367
+ const mppx = Mppx_client.create({
1368
+ polyfill: false,
1369
+ methods: [
1370
+ tempo_client({
1371
+ account: swapPayer,
1372
+ autoSwap: true,
1373
+ getClient() {
1374
+ return client
1375
+ },
1376
+ }),
1377
+ ],
1378
+ })
1379
+
1380
+ const httpServer = await Http.createServer(async (req, res) => {
1381
+ const result = await Mppx_server.toNodeListener(
1382
+ server.charge({
1383
+ amount: '999999999',
1384
+ currency: asset,
1385
+ recipient: accounts[0]!.address,
1386
+ }),
1387
+ )(req, res)
1388
+ if (result.status === 402) return
1389
+ res.end('OK')
1390
+ })
1391
+
1392
+ await expect(mppx.fetch(httpServer.url)).rejects.toThrow('Insufficient funds')
1393
+
1394
+ httpServer.close()
1395
+ })
1396
+
1397
+ test('error: throws when tokenIn list has no viable candidates', async () => {
1398
+ const bogusToken = '0x0000000000000000000000000000000000099999' as const
1399
+
1400
+ const mppx = Mppx_client.create({
1401
+ polyfill: false,
1402
+ methods: [
1403
+ tempo_client({
1404
+ account: brokePayer,
1405
+ autoSwap: { tokenIn: [bogusToken] },
1406
+ getClient() {
1407
+ return client
1408
+ },
1409
+ }),
1410
+ ],
1411
+ })
1412
+
1413
+ const httpServer = await Http.createServer(async (req, res) => {
1414
+ const result = await Mppx_server.toNodeListener(
1415
+ server.charge({
1416
+ amount: '1',
1417
+ currency: asset,
1418
+ recipient: accounts[0]!.address,
1419
+ }),
1420
+ )(req, res)
1421
+ if (result.status === 402) return
1422
+ res.end('OK')
1423
+ })
1424
+
1425
+ await expect(mppx.fetch(httpServer.url)).rejects.toThrow('Insufficient funds')
1426
+
1427
+ httpServer.close()
1428
+ })
1429
+ })
1059
1430
  })
@@ -1,10 +1,4 @@
1
- import {
2
- decodeFunctionData,
3
- isAddressEqual,
4
- parseEventLogs,
5
- type TransactionReceipt,
6
- toFunctionSelector,
7
- } from 'viem'
1
+ import { decodeFunctionData, isAddressEqual, parseEventLogs, type TransactionReceipt } from 'viem'
8
2
  import {
9
3
  getTransactionReceipt,
10
4
  sendRawTransaction,
@@ -19,18 +13,12 @@ import * as Method from '../../Method.js'
19
13
  import * as Client from '../../viem/Client.js'
20
14
  import * as Account from '../internal/account.js'
21
15
  import * as defaults from '../internal/defaults.js'
16
+ import * as FeePayer from '../internal/fee-payer.js'
17
+ import * as Selectors from '../internal/selectors.js'
22
18
  import { simulateTransaction } from '../internal/simulate.js'
23
19
  import type * as types from '../internal/types.js'
24
20
  import * as Methods from '../Methods.js'
25
21
 
26
- const transferSelector = /*#__PURE__*/ toFunctionSelector(
27
- 'function transfer(address to, uint256 amount)',
28
- )
29
-
30
- const transferWithMemoSelector = /*#__PURE__*/ toFunctionSelector(
31
- 'function transferWithMemo(address to, uint256 amount, bytes32 memo)',
32
- )
33
-
34
22
  /**
35
23
  * Creates a Tempo charge method intent for usage on the server.
36
24
  *
@@ -202,7 +190,7 @@ export function charge<const parameters extends charge.Parameters>(
202
190
  const selector = call.data.slice(0, 10)
203
191
 
204
192
  if (memo) {
205
- if (selector !== transferWithMemoSelector) return false
193
+ if (selector !== Selectors.transferWithMemo) return false
206
194
  try {
207
195
  const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
208
196
  const [to, amount_, memo_] = args as [`0x${string}`, bigint, `0x${string}`]
@@ -216,7 +204,7 @@ export function charge<const parameters extends charge.Parameters>(
216
204
  }
217
205
  }
218
206
 
219
- if (selector === transferSelector) {
207
+ if (selector === Selectors.transfer) {
220
208
  try {
221
209
  const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
222
210
  const [to, amount_] = args as [`0x${string}`, bigint]
@@ -226,7 +214,7 @@ export function charge<const parameters extends charge.Parameters>(
226
214
  }
227
215
  }
228
216
 
229
- if (selector === transferWithMemoSelector) {
217
+ if (selector === Selectors.transferWithMemo) {
230
218
  try {
231
219
  const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
232
220
  const [to, amount_] = args as [`0x${string}`, bigint, `0x${string}`]
@@ -246,6 +234,9 @@ export function charge<const parameters extends charge.Parameters>(
246
234
  recipient,
247
235
  })
248
236
 
237
+ if (feePayer && methodDetails?.feePayer !== false)
238
+ FeePayer.validateCalls(calls, { amount, currency, recipient })
239
+
249
240
  const serializedTransaction_final = await (async () => {
250
241
  if (feePayer && methodDetails?.feePayer !== false) {
251
242
  return signTransaction(client, {