mppx 0.3.11 → 0.3.13
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/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 +76 -11
- package/dist/client/internal/Fetch.js.map +1 -1
- package/dist/internal/constantTimeEqual.d.ts.map +1 -1
- package/dist/internal/constantTimeEqual.js +7 -4
- package/dist/internal/constantTimeEqual.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/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +8 -6
- package/dist/tempo/server/Charge.js.map +1 -1
- package/package.json +1 -1
- package/src/client/Mppx.test-d.ts +28 -0
- package/src/client/Mppx.ts +3 -3
- package/src/client/internal/Fetch.test.ts +454 -0
- package/src/client/internal/Fetch.ts +89 -14
- package/src/internal/constantTimeEqual.ts +6 -4
- 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/server/Charge.test.ts +374 -3
- package/src/tempo/server/Charge.ts +9 -18
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { encodeFunctionData } from 'viem'
|
|
2
|
+
import { Abis, Addresses } from 'viem/tempo'
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import { callScopes, FeePayerValidationError, validateCalls } from './fee-payer.js'
|
|
5
|
+
import * as Selectors from './selectors.js'
|
|
6
|
+
|
|
7
|
+
const details = { amount: '1', currency: '0x01', recipient: '0x02' }
|
|
8
|
+
const bogus = '0x0000000000000000000000000000000000000001' as const
|
|
9
|
+
|
|
10
|
+
describe('callScopes', () => {
|
|
11
|
+
test('has 4 allowed patterns', () => {
|
|
12
|
+
expect(callScopes).toHaveLength(4)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('patterns use correct selectors', () => {
|
|
16
|
+
expect(callScopes).toEqual([
|
|
17
|
+
[Selectors.transfer],
|
|
18
|
+
[Selectors.transferWithMemo],
|
|
19
|
+
[Selectors.approve, Selectors.swapExactAmountOut, Selectors.transfer],
|
|
20
|
+
[Selectors.approve, Selectors.swapExactAmountOut, Selectors.transferWithMemo],
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('validateCalls', () => {
|
|
26
|
+
test('accepts single transfer', () => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
validateCalls(
|
|
29
|
+
[
|
|
30
|
+
{
|
|
31
|
+
data: encodeFunctionData({
|
|
32
|
+
abi: Abis.tip20,
|
|
33
|
+
functionName: 'transfer',
|
|
34
|
+
args: [bogus, 100n],
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
details,
|
|
39
|
+
),
|
|
40
|
+
).not.toThrow()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('accepts approve + buy + transfer', () => {
|
|
44
|
+
const swapSelector = Selectors.swapExactAmountOut
|
|
45
|
+
expect(() =>
|
|
46
|
+
validateCalls(
|
|
47
|
+
[
|
|
48
|
+
{
|
|
49
|
+
data: encodeFunctionData({
|
|
50
|
+
abi: Abis.tip20,
|
|
51
|
+
functionName: 'approve',
|
|
52
|
+
args: [Addresses.stablecoinDex, 100n],
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
to: Addresses.stablecoinDex,
|
|
57
|
+
data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
data: encodeFunctionData({
|
|
61
|
+
abi: Abis.tip20,
|
|
62
|
+
functionName: 'transfer',
|
|
63
|
+
args: [bogus, 100n],
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
details,
|
|
68
|
+
),
|
|
69
|
+
).not.toThrow()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('error: rejects empty calls', () => {
|
|
73
|
+
expect(() => validateCalls([], details)).toThrow(FeePayerValidationError)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('error: rejects unknown selector', () => {
|
|
77
|
+
expect(() => validateCalls([{ data: '0xdeadbeef' as `0x${string}` }], details)).toThrow(
|
|
78
|
+
'disallowed call pattern',
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('error: rejects extra calls beyond allowed patterns', () => {
|
|
83
|
+
const swapSelector = Selectors.swapExactAmountOut
|
|
84
|
+
expect(() =>
|
|
85
|
+
validateCalls(
|
|
86
|
+
[
|
|
87
|
+
{
|
|
88
|
+
data: encodeFunctionData({
|
|
89
|
+
abi: Abis.tip20,
|
|
90
|
+
functionName: 'approve',
|
|
91
|
+
args: [Addresses.stablecoinDex, 100n],
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
to: Addresses.stablecoinDex,
|
|
96
|
+
data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
data: encodeFunctionData({
|
|
100
|
+
abi: Abis.tip20,
|
|
101
|
+
functionName: 'transfer',
|
|
102
|
+
args: [bogus, 100n],
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
data: encodeFunctionData({
|
|
107
|
+
abi: Abis.tip20,
|
|
108
|
+
functionName: 'transfer',
|
|
109
|
+
args: [bogus, 100n],
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
details,
|
|
114
|
+
),
|
|
115
|
+
).toThrow('disallowed call pattern')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('error: rejects wrong order (transfer before approve + buy)', () => {
|
|
119
|
+
const swapSelector = Selectors.swapExactAmountOut
|
|
120
|
+
expect(() =>
|
|
121
|
+
validateCalls(
|
|
122
|
+
[
|
|
123
|
+
{
|
|
124
|
+
data: encodeFunctionData({
|
|
125
|
+
abi: Abis.tip20,
|
|
126
|
+
functionName: 'transfer',
|
|
127
|
+
args: [bogus, 100n],
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
data: encodeFunctionData({
|
|
132
|
+
abi: Abis.tip20,
|
|
133
|
+
functionName: 'approve',
|
|
134
|
+
args: [Addresses.stablecoinDex, 100n],
|
|
135
|
+
}),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
to: Addresses.stablecoinDex,
|
|
139
|
+
data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
details,
|
|
143
|
+
),
|
|
144
|
+
).toThrow('disallowed call pattern')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('error: rejects approve with non-DEX spender', () => {
|
|
148
|
+
const swapSelector = Selectors.swapExactAmountOut
|
|
149
|
+
expect(() =>
|
|
150
|
+
validateCalls(
|
|
151
|
+
[
|
|
152
|
+
{
|
|
153
|
+
data: encodeFunctionData({
|
|
154
|
+
abi: Abis.tip20,
|
|
155
|
+
functionName: 'approve',
|
|
156
|
+
args: [bogus, 100n],
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
to: Addresses.stablecoinDex,
|
|
161
|
+
data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
data: encodeFunctionData({
|
|
165
|
+
abi: Abis.tip20,
|
|
166
|
+
functionName: 'transfer',
|
|
167
|
+
args: [bogus, 100n],
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
details,
|
|
172
|
+
),
|
|
173
|
+
).toThrow('approve spender is not the DEX')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('error: rejects buy targeting non-DEX address', () => {
|
|
177
|
+
const swapSelector = Selectors.swapExactAmountOut
|
|
178
|
+
expect(() =>
|
|
179
|
+
validateCalls(
|
|
180
|
+
[
|
|
181
|
+
{
|
|
182
|
+
data: encodeFunctionData({
|
|
183
|
+
abi: Abis.tip20,
|
|
184
|
+
functionName: 'approve',
|
|
185
|
+
args: [Addresses.stablecoinDex, 100n],
|
|
186
|
+
}),
|
|
187
|
+
},
|
|
188
|
+
{ to: bogus, data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}` },
|
|
189
|
+
{
|
|
190
|
+
data: encodeFunctionData({
|
|
191
|
+
abi: Abis.tip20,
|
|
192
|
+
functionName: 'transfer',
|
|
193
|
+
args: [bogus, 100n],
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
details,
|
|
198
|
+
),
|
|
199
|
+
).toThrow('buy target is not the DEX')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('error: rejects approve + buy without transfer', () => {
|
|
203
|
+
const swapSelector = Selectors.swapExactAmountOut
|
|
204
|
+
expect(() =>
|
|
205
|
+
validateCalls(
|
|
206
|
+
[
|
|
207
|
+
{
|
|
208
|
+
data: encodeFunctionData({
|
|
209
|
+
abi: Abis.tip20,
|
|
210
|
+
functionName: 'approve',
|
|
211
|
+
args: [Addresses.stablecoinDex, 100n],
|
|
212
|
+
}),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
to: Addresses.stablecoinDex,
|
|
216
|
+
data: `${swapSelector}${'00'.repeat(128)}` as `0x${string}`,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
details,
|
|
220
|
+
),
|
|
221
|
+
).toThrow('disallowed call pattern')
|
|
222
|
+
})
|
|
223
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { decodeFunctionData, isAddressEqual } from 'viem'
|
|
2
|
+
import { Abis, Addresses } from 'viem/tempo'
|
|
3
|
+
import * as Selectors from './selectors.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Allowed call patterns for fee-payer sponsored transactions.
|
|
7
|
+
* Each inner array is an ordered list of function selectors.
|
|
8
|
+
*/
|
|
9
|
+
export const callScopes = [
|
|
10
|
+
[Selectors.transfer],
|
|
11
|
+
[Selectors.transferWithMemo],
|
|
12
|
+
[Selectors.approve, Selectors.swapExactAmountOut, Selectors.transfer],
|
|
13
|
+
[Selectors.approve, Selectors.swapExactAmountOut, Selectors.transferWithMemo],
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
/** Validates that a set of transaction calls matches an allowed fee-payer pattern. */
|
|
17
|
+
export function validateCalls(
|
|
18
|
+
calls: readonly { data?: `0x${string}` | undefined; to?: `0x${string}` | undefined }[],
|
|
19
|
+
details: Record<string, string>,
|
|
20
|
+
) {
|
|
21
|
+
const callSelectors = calls.map((c) => c.data?.slice(0, 10))
|
|
22
|
+
const allowed = callScopes.some(
|
|
23
|
+
(pattern) =>
|
|
24
|
+
pattern.length === callSelectors.length &&
|
|
25
|
+
pattern.every((sel, i) => sel === callSelectors[i]),
|
|
26
|
+
)
|
|
27
|
+
if (!allowed)
|
|
28
|
+
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
|
|
29
|
+
|
|
30
|
+
// Validate approve spender and buy target are the DEX.
|
|
31
|
+
const approveCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.approve)
|
|
32
|
+
if (approveCall) {
|
|
33
|
+
const { args } = decodeFunctionData({ abi: Abis.tip20, data: approveCall.data! })
|
|
34
|
+
if (!isAddressEqual((args as [`0x${string}`])[0]!, Addresses.stablecoinDex))
|
|
35
|
+
throw new FeePayerValidationError('approve spender is not the DEX', details)
|
|
36
|
+
}
|
|
37
|
+
const buyCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.swapExactAmountOut)
|
|
38
|
+
if (buyCall && (!buyCall.to || !isAddressEqual(buyCall.to, Addresses.stablecoinDex)))
|
|
39
|
+
throw new FeePayerValidationError('buy target is not the DEX', details)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class FeePayerValidationError extends Error {
|
|
43
|
+
override readonly name = 'FeePayerValidationError'
|
|
44
|
+
|
|
45
|
+
constructor(reason: string, details: Record<string, string>) {
|
|
46
|
+
super(
|
|
47
|
+
[
|
|
48
|
+
`Invalid transaction: ${reason}`,
|
|
49
|
+
...Object.entries(details).map(([k, v]) => ` - ${k}: ${v}`),
|
|
50
|
+
].join('\n'),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AbiItem } from 'ox'
|
|
2
|
+
import { Abis } from 'viem/tempo'
|
|
3
|
+
|
|
4
|
+
export const approve = /*#__PURE__*/ AbiItem.getSelector(Abis.tip20, 'approve')
|
|
5
|
+
export const transfer = /*#__PURE__*/ AbiItem.getSelector(Abis.tip20, 'transfer')
|
|
6
|
+
export const transferWithMemo = /*#__PURE__*/ AbiItem.getSelector(Abis.tip20, 'transferWithMemo')
|
|
7
|
+
export const swapExactAmountOut = /*#__PURE__*/ AbiItem.getSelector(
|
|
8
|
+
Abis.stablecoinDex,
|
|
9
|
+
'swapExactAmountOut',
|
|
10
|
+
)
|
|
@@ -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,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
|
})
|