mppx 0.3.9 → 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.
- package/README.md +3 -3
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +2 -0
- package/dist/Challenge.js.map +1 -1
- package/dist/Errors.d.ts +0 -2
- package/dist/Errors.d.ts.map +1 -1
- package/dist/Errors.js +1 -3
- package/dist/Errors.js.map +1 -1
- package/dist/client/Mppx.d.ts +1 -1
- package/dist/client/Mppx.d.ts.map +1 -1
- package/dist/client/internal/Fetch.d.ts +1 -1
- package/dist/client/internal/Fetch.d.ts.map +1 -1
- package/dist/client/internal/Fetch.js +23 -4
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/internal/constantTimeEqual.d.ts.map +1 -1
- package/dist/internal/constantTimeEqual.js +4 -6
- package/dist/internal/constantTimeEqual.js.map +1 -1
- package/dist/internal/env.d.ts +2 -2
- package/dist/internal/env.d.ts.map +1 -1
- package/dist/internal/env.js +1 -2
- package/dist/internal/env.js.map +1 -1
- package/dist/middlewares/internal/mppx.d.ts.map +1 -1
- package/dist/middlewares/internal/mppx.js +6 -2
- package/dist/middlewares/internal/mppx.js.map +1 -1
- package/dist/server/Mppx.d.ts +13 -3
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +46 -3
- package/dist/server/Mppx.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +10 -0
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +23 -9
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +1 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/auto-swap.d.ts +49 -0
- package/dist/tempo/internal/auto-swap.d.ts.map +1 -0
- package/dist/tempo/internal/auto-swap.js +89 -0
- package/dist/tempo/internal/auto-swap.js.map +1 -0
- package/dist/tempo/internal/fee-payer.d.ts +15 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -0
- package/dist/tempo/internal/fee-payer.js +41 -0
- package/dist/tempo/internal/fee-payer.js.map +1 -0
- package/dist/tempo/internal/selectors.d.ts +5 -0
- package/dist/tempo/internal/selectors.d.ts.map +1 -0
- package/dist/tempo/internal/selectors.js +7 -0
- package/dist/tempo/internal/selectors.js.map +1 -0
- package/dist/tempo/internal/simulate.d.ts +21 -0
- package/dist/tempo/internal/simulate.d.ts.map +1 -0
- package/dist/tempo/internal/simulate.js +31 -0
- package/dist/tempo/internal/simulate.js.map +1 -0
- package/dist/tempo/server/Charge.d.ts +12 -0
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +36 -12
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +14 -0
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +59 -40
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts +3 -0
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +27 -6
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +1 -1
- package/src/Challenge.ts +2 -0
- package/src/Errors.test.ts +43 -18
- package/src/Errors.ts +1 -4
- package/src/client/Mppx.test-d.ts +28 -0
- package/src/client/Mppx.test.ts +1 -0
- package/src/client/Mppx.ts +3 -3
- package/src/client/internal/Fetch.test.ts +410 -0
- package/src/client/internal/Fetch.ts +25 -7
- package/src/internal/constantTimeEqual.ts +5 -4
- package/src/internal/env.test.ts +2 -2
- package/src/internal/env.ts +4 -5
- package/src/middlewares/express.test.ts +5 -0
- package/src/middlewares/hono.test.ts +5 -0
- package/src/middlewares/internal/mppx.ts +5 -2
- package/src/middlewares/nextjs.test.ts +5 -0
- package/src/proxy/Proxy.test.ts +3 -0
- package/src/proxy/services/openai.test.ts +3 -0
- package/src/server/Mppx.test.ts +93 -2
- package/src/server/Mppx.ts +81 -6
- package/src/tempo/client/Charge.ts +40 -9
- package/src/tempo/internal/auto-swap.test.ts +113 -0
- package/src/tempo/internal/auto-swap.ts +141 -0
- package/src/tempo/internal/fee-payer.test.ts +223 -0
- package/src/tempo/internal/fee-payer.ts +53 -0
- package/src/tempo/internal/selectors.ts +10 -0
- package/src/tempo/internal/simulate.ts +49 -0
- package/src/tempo/server/Charge.test.ts +436 -3
- package/src/tempo/server/Charge.ts +52 -23
- package/src/tempo/server/Session.test.ts +49 -0
- package/src/tempo/server/Session.ts +76 -34
- package/src/tempo/session/Chain.test.ts +36 -0
- package/src/tempo/session/Chain.ts +38 -2
|
@@ -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 {
|
|
6
|
-
import {
|
|
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,133 @@ 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
|
+
})
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
describe('intent: charge; type: transaction; waitForConfirmation: false', () => {
|
|
595
|
+
test('returns receipt without waiting for confirmation', async () => {
|
|
596
|
+
const serverNoWait = Mppx_server.create({
|
|
597
|
+
methods: [
|
|
598
|
+
tempo_server.charge({
|
|
599
|
+
getClient() {
|
|
600
|
+
return client
|
|
601
|
+
},
|
|
602
|
+
currency: asset,
|
|
603
|
+
account: accounts[0],
|
|
604
|
+
waitForConfirmation: false,
|
|
605
|
+
}),
|
|
606
|
+
],
|
|
607
|
+
realm,
|
|
608
|
+
secretKey,
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
const mppx = Mppx_client.create({
|
|
612
|
+
polyfill: false,
|
|
613
|
+
methods: [
|
|
614
|
+
tempo_client({
|
|
615
|
+
account: accounts[1],
|
|
616
|
+
getClient() {
|
|
617
|
+
return client
|
|
618
|
+
},
|
|
619
|
+
}),
|
|
620
|
+
],
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
624
|
+
const result = await Mppx_server.toNodeListener(
|
|
625
|
+
serverNoWait.charge({
|
|
626
|
+
amount: '1',
|
|
627
|
+
currency: asset,
|
|
628
|
+
recipient: accounts[0].address,
|
|
629
|
+
}),
|
|
630
|
+
)(req, res)
|
|
631
|
+
if (result.status === 402) return
|
|
632
|
+
res.end('OK')
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
const response = await fetch(httpServer.url)
|
|
636
|
+
expect(response.status).toBe(402)
|
|
637
|
+
|
|
638
|
+
const credential = await mppx.createCredential(response)
|
|
639
|
+
|
|
640
|
+
{
|
|
641
|
+
const response = await fetch(httpServer.url, {
|
|
642
|
+
headers: { Authorization: credential },
|
|
643
|
+
})
|
|
644
|
+
expect(response.status).toBe(200)
|
|
645
|
+
|
|
646
|
+
const receipt = Receipt.fromResponse(response)
|
|
647
|
+
expect(receipt.status).toBe('success')
|
|
648
|
+
expect(receipt.method).toBe('tempo')
|
|
649
|
+
expect(receipt.reference).toBeDefined()
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
httpServer.close()
|
|
653
|
+
})
|
|
525
654
|
})
|
|
526
655
|
|
|
527
656
|
describe('intent: unknown', () => {
|
|
@@ -994,4 +1123,308 @@ describe('tempo', () => {
|
|
|
994
1123
|
httpServer.close()
|
|
995
1124
|
})
|
|
996
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
|
+
})
|
|
997
1430
|
})
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { decodeFunctionData, isAddressEqual, parseEventLogs, type TransactionReceipt } from 'viem'
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from 'viem'
|
|
8
|
-
import { getTransactionReceipt, sendRawTransactionSync, signTransaction } from 'viem/actions'
|
|
3
|
+
getTransactionReceipt,
|
|
4
|
+
sendRawTransaction,
|
|
5
|
+
sendRawTransactionSync,
|
|
6
|
+
signTransaction,
|
|
7
|
+
} from 'viem/actions'
|
|
9
8
|
import { tempo as tempo_chain } from 'viem/chains'
|
|
10
9
|
import { Abis, Transaction } from 'viem/tempo'
|
|
11
10
|
import { PaymentExpiredError } from '../../Errors.js'
|
|
@@ -14,17 +13,12 @@ import * as Method from '../../Method.js'
|
|
|
14
13
|
import * as Client from '../../viem/Client.js'
|
|
15
14
|
import * as Account from '../internal/account.js'
|
|
16
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'
|
|
18
|
+
import { simulateTransaction } from '../internal/simulate.js'
|
|
17
19
|
import type * as types from '../internal/types.js'
|
|
18
20
|
import * as Methods from '../Methods.js'
|
|
19
21
|
|
|
20
|
-
const transferSelector = /*#__PURE__*/ toFunctionSelector(
|
|
21
|
-
'function transfer(address to, uint256 amount)',
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
const transferWithMemoSelector = /*#__PURE__*/ toFunctionSelector(
|
|
25
|
-
'function transferWithMemo(address to, uint256 amount, bytes32 memo)',
|
|
26
|
-
)
|
|
27
|
-
|
|
28
22
|
/**
|
|
29
23
|
* Creates a Tempo charge method intent for usage on the server.
|
|
30
24
|
*
|
|
@@ -45,6 +39,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
45
39
|
description,
|
|
46
40
|
externalId,
|
|
47
41
|
memo,
|
|
42
|
+
waitForConfirmation = true,
|
|
48
43
|
} = parameters
|
|
49
44
|
|
|
50
45
|
const { recipient, feePayer } = Account.resolve(parameters)
|
|
@@ -195,7 +190,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
195
190
|
const selector = call.data.slice(0, 10)
|
|
196
191
|
|
|
197
192
|
if (memo) {
|
|
198
|
-
if (selector !==
|
|
193
|
+
if (selector !== Selectors.transferWithMemo) return false
|
|
199
194
|
try {
|
|
200
195
|
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
201
196
|
const [to, amount_, memo_] = args as [`0x${string}`, bigint, `0x${string}`]
|
|
@@ -209,7 +204,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
209
204
|
}
|
|
210
205
|
}
|
|
211
206
|
|
|
212
|
-
if (selector ===
|
|
207
|
+
if (selector === Selectors.transfer) {
|
|
213
208
|
try {
|
|
214
209
|
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
215
210
|
const [to, amount_] = args as [`0x${string}`, bigint]
|
|
@@ -219,7 +214,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
219
214
|
}
|
|
220
215
|
}
|
|
221
216
|
|
|
222
|
-
if (selector ===
|
|
217
|
+
if (selector === Selectors.transferWithMemo) {
|
|
223
218
|
try {
|
|
224
219
|
const { args } = decodeFunctionData({ abi: Abis.tip20, data: call.data })
|
|
225
220
|
const [to, amount_] = args as [`0x${string}`, bigint, `0x${string}`]
|
|
@@ -239,6 +234,9 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
239
234
|
recipient,
|
|
240
235
|
})
|
|
241
236
|
|
|
237
|
+
if (feePayer && methodDetails?.feePayer !== false)
|
|
238
|
+
FeePayer.validateCalls(calls, { amount, currency, recipient })
|
|
239
|
+
|
|
242
240
|
const serializedTransaction_final = await (async () => {
|
|
243
241
|
if (feePayer && methodDetails?.feePayer !== false) {
|
|
244
242
|
return signTransaction(client, {
|
|
@@ -250,11 +248,30 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
250
248
|
return serializedTransaction
|
|
251
249
|
})()
|
|
252
250
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
if (waitForConfirmation) {
|
|
252
|
+
const receipt = await sendRawTransactionSync(client, {
|
|
253
|
+
serializedTransaction: serializedTransaction_final,
|
|
254
|
+
})
|
|
255
|
+
return toReceipt(receipt)
|
|
256
|
+
} else {
|
|
257
|
+
// Optimistic path: simulate to catch obvious reverts, then broadcast
|
|
258
|
+
// without waiting for on-chain confirmation. The returned receipt
|
|
259
|
+
// assumes success — callers opt into this risk via waitForConfirmation: false.
|
|
260
|
+
await simulateTransaction(client, {
|
|
261
|
+
...transaction,
|
|
262
|
+
from: transaction.from as `0x${string}`,
|
|
263
|
+
calls,
|
|
264
|
+
})
|
|
265
|
+
const hash = await sendRawTransaction(client, {
|
|
266
|
+
serializedTransaction: serializedTransaction_final,
|
|
267
|
+
})
|
|
268
|
+
return {
|
|
269
|
+
method: 'tempo',
|
|
270
|
+
status: 'success',
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
reference: hash,
|
|
273
|
+
} as const
|
|
274
|
+
}
|
|
258
275
|
}
|
|
259
276
|
|
|
260
277
|
default:
|
|
@@ -270,6 +287,18 @@ export declare namespace charge {
|
|
|
270
287
|
type Parameters = {
|
|
271
288
|
/** Testnet mode. */
|
|
272
289
|
testnet?: boolean | undefined
|
|
290
|
+
/**
|
|
291
|
+
* Whether to wait for the charge transaction to confirm on-chain before
|
|
292
|
+
* responding. @default true
|
|
293
|
+
*
|
|
294
|
+
* When `false`, the transaction is simulated via `eth_estimateGas` and
|
|
295
|
+
* broadcast without waiting for inclusion. The receipt will optimistically
|
|
296
|
+
* report `status: 'success'` based on simulation alone — if the
|
|
297
|
+
* transaction reverts on-chain after broadcast (e.g. due to a state
|
|
298
|
+
* change between simulation and inclusion), the receipt will not reflect
|
|
299
|
+
* the failure.
|
|
300
|
+
*/
|
|
301
|
+
waitForConfirmation?: boolean | undefined
|
|
273
302
|
} & Client.getResolver.Parameters &
|
|
274
303
|
Account.resolve.Parameters &
|
|
275
304
|
Defaults
|
|
@@ -1101,6 +1101,55 @@ describe('session', () => {
|
|
|
1101
1101
|
expect(result).toBeUndefined()
|
|
1102
1102
|
})
|
|
1103
1103
|
|
|
1104
|
+
test('returns undefined for open POST with content-length > 0 (content request)', () => {
|
|
1105
|
+
const server = createServer()
|
|
1106
|
+
const result = server.respond!({
|
|
1107
|
+
credential: {
|
|
1108
|
+
challenge: makeChallenge({
|
|
1109
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
|
|
1110
|
+
}),
|
|
1111
|
+
payload: { action: 'open' },
|
|
1112
|
+
},
|
|
1113
|
+
input: new Request('http://localhost', {
|
|
1114
|
+
method: 'POST',
|
|
1115
|
+
headers: { 'content-length': '42' },
|
|
1116
|
+
}),
|
|
1117
|
+
} as any)
|
|
1118
|
+
expect(result).toBeUndefined()
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
test('returns undefined for open POST with transfer-encoding header (content request)', () => {
|
|
1122
|
+
const server = createServer()
|
|
1123
|
+
const result = server.respond!({
|
|
1124
|
+
credential: {
|
|
1125
|
+
challenge: makeChallenge({
|
|
1126
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
|
|
1127
|
+
}),
|
|
1128
|
+
payload: { action: 'open' },
|
|
1129
|
+
},
|
|
1130
|
+
input: new Request('http://localhost', {
|
|
1131
|
+
method: 'POST',
|
|
1132
|
+
headers: { 'transfer-encoding': 'chunked' },
|
|
1133
|
+
}),
|
|
1134
|
+
} as any)
|
|
1135
|
+
expect(result).toBeUndefined()
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
test('returns 204 for GET with topUp action', () => {
|
|
1139
|
+
const server = createServer()
|
|
1140
|
+
const result = server.respond!({
|
|
1141
|
+
credential: {
|
|
1142
|
+
challenge: makeChallenge({
|
|
1143
|
+
channelId: '0x0000000000000000000000000000000000000000000000000000000000000001' as Hex,
|
|
1144
|
+
}),
|
|
1145
|
+
payload: { action: 'topUp' },
|
|
1146
|
+
},
|
|
1147
|
+
input: new Request('http://localhost', { method: 'GET' }),
|
|
1148
|
+
} as any)
|
|
1149
|
+
expect(result).toBeInstanceOf(Response)
|
|
1150
|
+
expect((result as Response).status).toBe(204)
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1104
1153
|
test('returns undefined for voucher POST with content-length > 0 (content request)', () => {
|
|
1105
1154
|
const server = createServer()
|
|
1106
1155
|
const result = server.respond!({
|